This commit is contained in:
Tyrrrz
2021-04-16 23:09:08 +03:00
parent 2fea455c64
commit 511af1e35c
38 changed files with 173 additions and 377 deletions

View File

@@ -5,7 +5,6 @@ using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using FileSize = DiscordChatExporter.Core.Discord.Data.Common.FileSize;
namespace DiscordChatExporter.Core.Discord.Data
{

View File

@@ -1,4 +1,6 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace DiscordChatExporter.Core.Discord.Data.Common
{
@@ -61,5 +63,37 @@ namespace DiscordChatExporter.Core.Discord.Data.Common
public partial struct FileSize
{
public static FileSize FromBytes(long bytes) => new(bytes);
public static FileSize? TryParse(string value)
{
var match = Regex.Match(value, @"^(\d+[\.,]?\d*)\s*(\w)?b$", RegexOptions.IgnoreCase);
// Number part
if (!double.TryParse(
match.Groups[1].Value,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var number))
{
return null;
}
// Magnitude part
var magnitude = match.Groups[2].Value.ToUpperInvariant() switch
{
"G" => 1_000_000_000,
"M" => 1_000_000,
"K" => 1_000,
"" => 1,
_ => -1
};
if (magnitude < 0)
{
return null;
}
return FromBytes((long) (number * magnitude));
}
}
}
}

View File

@@ -48,7 +48,7 @@ namespace DiscordChatExporter.Core.Discord
}
public static Snowflake Parse(string str, IFormatProvider? formatProvider) =>
TryParse(str, formatProvider) ?? throw new FormatException($"Invalid snowflake: {str}.");
TryParse(str, formatProvider) ?? throw new FormatException($"Invalid snowflake '{str}'.");
public static Snowflake Parse(string str) => Parse(str, null);
}

View File

@@ -5,7 +5,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ByteSize" Version="2.0.0" />
<PackageReference Include="JsonExtensions" Version="1.0.1" />
<PackageReference Include="MiniRazor.CodeGen" Version="2.1.2" />
<PackageReference Include="Polly" Version="7.2.1" />

View File

@@ -4,6 +4,7 @@ using System.Text;
using System.Text.RegularExpressions;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exporting.Partitioning;
using DiscordChatExporter.Core.Utils;
namespace DiscordChatExporter.Core.Exporting
@@ -28,7 +29,7 @@ namespace DiscordChatExporter.Core.Exporting
public Snowflake? Before { get; }
public IPartitioner Partitoner { get; }
public PartitionLimit PartitionLimit { get; }
public bool ShouldDownloadMedia { get; }
@@ -43,7 +44,7 @@ namespace DiscordChatExporter.Core.Exporting
ExportFormat format,
Snowflake? after,
Snowflake? before,
IPartitioner partitioner,
PartitionLimit partitionLimit,
bool shouldDownloadMedia,
bool shouldReuseMedia,
string dateFormat)
@@ -54,7 +55,7 @@ namespace DiscordChatExporter.Core.Exporting
Format = format;
After = after;
Before = before;
Partitoner = partitioner;
PartitionLimit = partitionLimit;
ShouldDownloadMedia = shouldDownloadMedia;
ShouldReuseMedia = shouldReuseMedia;
DateFormat = dateFormat;

View File

@@ -1,21 +1,15 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ByteSizeLib;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Core.Exporting.Partitioners;
using DiscordChatExporter.Core.Exporting.Writers;
namespace DiscordChatExporter.Core.Exporting
{
internal partial class MessageExporter : IAsyncDisposable
{
private readonly ExportContext _context;
private long _messageCount;
private int _partitionIndex;
private MessageWriter? _writer;
@@ -24,17 +18,6 @@ namespace DiscordChatExporter.Core.Exporting
_context = context;
}
private bool IsPartitionLimitReached()
{
if (_writer is null)
{
return false;
}
return _context.Request.Partitoner.IsLimitReached(
new ExportPartitioningContext(_messageCount, _writer.SizeInBytes));
}
private async ValueTask ResetWriterAsync()
{
if (_writer is not null)
@@ -48,7 +31,8 @@ namespace DiscordChatExporter.Core.Exporting
private async ValueTask<MessageWriter> GetWriterAsync()
{
// Ensure partition limit has not been exceeded
if (_writer != null && IsPartitionLimitReached())
if (_writer is not null &&
_context.Request.PartitionLimit.IsReached(_writer.MessagesWritten, _writer.BytesWritten))
{
await ResetWriterAsync();
_partitionIndex++;
@@ -74,7 +58,6 @@ namespace DiscordChatExporter.Core.Exporting
{
var writer = await GetWriterAsync();
await writer.WriteMessageAsync(message);
_messageCount++;
}
public async ValueTask DisposeAsync() => await ResetWriterAsync();
@@ -82,9 +65,7 @@ namespace DiscordChatExporter.Core.Exporting
internal partial class MessageExporter
{
private static string GetPartitionFilePath(
string baseFilePath,
int partitionIndex)
private static string GetPartitionFilePath(string baseFilePath, int partitionIndex)
{
// First partition - don't change file name
if (partitionIndex <= 0)

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DiscordChatExporter.Core.Exporting.Partitioners
{
public class ExportPartitioningContext
{
public long MessageCount { get; }
public long SizeInBytes { get; }
public ExportPartitioningContext(long messageCount, long sizeInBytes)
{
MessageCount = messageCount;
SizeInBytes = sizeInBytes;
}
}
}

View File

@@ -1,21 +0,0 @@
using DiscordChatExporter.Core.Exporting.Partitioners;
using System;
using System.Collections.Generic;
using System.Text;
namespace DiscordChatExporter.Core.Exporting
{
public class FileSizePartitioner : IPartitioner
{
private long _bytesPerFile;
public FileSizePartitioner(long bytesPerFile)
{
_bytesPerFile = bytesPerFile;
}
public bool IsLimitReached(ExportPartitioningContext context)
{
return context.SizeInBytes >= _bytesPerFile;
}
}
}

View File

@@ -1,12 +0,0 @@
using DiscordChatExporter.Core.Exporting.Partitioners;
using System;
using System.Collections.Generic;
using System.Text;
namespace DiscordChatExporter.Core.Exporting
{
public interface IPartitioner
{
bool IsLimitReached(ExportPartitioningContext context);
}
}

View File

@@ -1,25 +0,0 @@
using DiscordChatExporter.Core.Exporting.Partitioners;
using System;
using System.Collections.Generic;
using System.Text;
namespace DiscordChatExporter.Core.Exporting
{
public class MessageCountPartitioner : IPartitioner
{
private int _messagesPerPartition;
public MessageCountPartitioner(int messagesPerPartition)
{
_messagesPerPartition = messagesPerPartition;
}
public bool IsLimitReached(ExportPartitioningContext context)
{
return context.MessageCount > 0 &&
_messagesPerPartition != 0 &&
context.MessageCount % _messagesPerPartition == 0;
}
}
}

View File

@@ -1,16 +0,0 @@
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Core.Exporting.Partitioners;
using System;
using System.Collections.Generic;
using System.Text;
namespace DiscordChatExporter.Core.Exporting
{
public class NullPartitioner : IPartitioner
{
public bool IsLimitReached(ExportPartitioningContext context)
{
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace DiscordChatExporter.Core.Exporting.Partitioning
{
public class FileSizePartitionLimit : PartitionLimit
{
private readonly long _limit;
public FileSizePartitionLimit(long limit) => _limit = limit;
public override bool IsReached(long messagesWritten, long bytesWritten) =>
bytesWritten >= _limit;
}
}

View File

@@ -0,0 +1,12 @@
namespace DiscordChatExporter.Core.Exporting.Partitioning
{
public class MessageCountPartitionLimit : PartitionLimit
{
private readonly long _limit;
public MessageCountPartitionLimit(long limit) => _limit = limit;
public override bool IsReached(long messagesWritten, long bytesWritten) =>
messagesWritten >= _limit;
}
}

View File

@@ -0,0 +1,9 @@
namespace DiscordChatExporter.Core.Exporting.Partitioning
{
public class NullPartitionLimit : PartitionLimit
{
public static NullPartitionLimit Instance { get; } = new();
public override bool IsReached(long messagesWritten, long bytesWritten) => false;
}
}

View File

@@ -0,0 +1,22 @@
using DiscordChatExporter.Core.Discord.Data.Common;
namespace DiscordChatExporter.Core.Exporting.Partitioning
{
public abstract partial class PartitionLimit
{
public abstract bool IsReached(long messagesWritten, long bytesWritten);
}
public partial class PartitionLimit
{
public static PartitionLimit Parse(string value)
{
var fileSize = FileSize.TryParse(value);
if (fileSize is not null)
return new FileSizePartitionLimit(fileSize.Value.TotalBytes);
var messageCount = int.Parse(value);
return new MessageCountPartitionLimit(messageCount);
}
}
}

View File

@@ -58,6 +58,8 @@ namespace DiscordChatExporter.Core.Exporting.Writers
public override async ValueTask WriteMessageAsync(Message message)
{
await base.WriteMessageAsync(message);
// Author ID
await _writer.WriteAsync(CsvEncode(message.Author.Id.ToString()));
await _writer.WriteAsync(',');

View File

@@ -1,10 +1,10 @@
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.LayoutTemplateContext>
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.PostambleTemplateContext>
</div>
<div class="postamble">
<div class="postamble__entry">Exported @Model.MessageCount.ToString("N0") message(s)</div>
<div class="postamble__entry">Exported @Model.MessagesWritten.ToString("N0") message(s)</div>
</div>
</body>

View File

@@ -0,0 +1,15 @@
namespace DiscordChatExporter.Core.Exporting.Writers.Html
{
internal class PostambleTemplateContext
{
public ExportContext ExportContext { get; }
public long MessagesWritten { get; }
public PostambleTemplateContext(ExportContext exportContext, long messagesWritten)
{
ExportContext = exportContext;
MessagesWritten = messagesWritten;
}
}
}

View File

@@ -2,7 +2,7 @@
@using System.Threading.Tasks
@using Tyrrrz.Extensions
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.LayoutTemplateContext>
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.PreambleTemplateContext>
@{
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);

View File

@@ -1,18 +1,15 @@
namespace DiscordChatExporter.Core.Exporting.Writers.Html
{
internal class LayoutTemplateContext
internal class PreambleTemplateContext
{
public ExportContext ExportContext { get; }
public string ThemeName { get; }
public long MessageCount { get; }
public LayoutTemplateContext(ExportContext exportContext, string themeName, long messageCount)
public PreambleTemplateContext(ExportContext exportContext, string themeName)
{
ExportContext = exportContext;
ThemeName = themeName;
MessageCount = messageCount;
}
}
}

View File

@@ -14,8 +14,6 @@ namespace DiscordChatExporter.Core.Exporting.Writers
private readonly List<Message> _messageGroupBuffer = new();
private long _messageCount;
public HtmlMessageWriter(Stream stream, ExportContext context, string themeName)
: base(stream, context)
{
@@ -25,7 +23,7 @@ namespace DiscordChatExporter.Core.Exporting.Writers
public override async ValueTask WritePreambleAsync()
{
var templateContext = new LayoutTemplateContext(Context, _themeName, _messageCount);
var templateContext = new PreambleTemplateContext(Context, _themeName);
await _writer.WriteLineAsync(
await PreambleTemplate.RenderAsync(templateContext)
@@ -43,6 +41,8 @@ namespace DiscordChatExporter.Core.Exporting.Writers
public override async ValueTask WriteMessageAsync(Message message)
{
await base.WriteMessageAsync(message);
// If message group is empty or the given message can be grouped, buffer the given message
if (!_messageGroupBuffer.Any() || MessageGroup.CanJoin(_messageGroupBuffer.Last(), message))
{
@@ -56,9 +56,6 @@ namespace DiscordChatExporter.Core.Exporting.Writers
_messageGroupBuffer.Clear();
_messageGroupBuffer.Add(message);
}
// Increment message count
_messageCount++;
}
public override async ValueTask WritePostambleAsync()
@@ -67,7 +64,7 @@ namespace DiscordChatExporter.Core.Exporting.Writers
if (_messageGroupBuffer.Any())
await WriteMessageGroupAsync(MessageGroup.Join(_messageGroupBuffer));
var templateContext = new LayoutTemplateContext(Context, _themeName, _messageCount);
var templateContext = new PostambleTemplateContext(Context, MessagesWritten);
await _writer.WriteLineAsync(
await PostambleTemplate.RenderAsync(templateContext)

View File

@@ -13,8 +13,6 @@ namespace DiscordChatExporter.Core.Exporting.Writers
{
private readonly Utf8JsonWriter _writer;
private long _messageCount;
public JsonMessageWriter(Stream stream, ExportContext context)
: base(stream, context)
{
@@ -211,6 +209,8 @@ namespace DiscordChatExporter.Core.Exporting.Writers
public override async ValueTask WriteMessageAsync(Message message)
{
await base.WriteMessageAsync(message);
_writer.WriteStartObject();
// Metadata
@@ -279,8 +279,6 @@ namespace DiscordChatExporter.Core.Exporting.Writers
_writer.WriteEndObject();
await _writer.FlushAsync();
_messageCount++;
}
public override async ValueTask WritePostambleAsync()
@@ -288,7 +286,7 @@ namespace DiscordChatExporter.Core.Exporting.Writers
// Message array (end)
_writer.WriteEndArray();
_writer.WriteNumber("messageCount", _messageCount);
_writer.WriteNumber("messageCount", MessagesWritten);
// Root object (end)
_writer.WriteEndObject();

View File

@@ -11,17 +11,23 @@ namespace DiscordChatExporter.Core.Exporting.Writers
protected ExportContext Context { get; }
public long MessagesWritten { get; private set; }
public long BytesWritten => Stream.Length;
protected MessageWriter(Stream stream, ExportContext context)
{
Stream = stream;
Context = context;
}
public long SizeInBytes => Stream.Length;
public virtual ValueTask WritePreambleAsync() => default;
public abstract ValueTask WriteMessageAsync(Message message);
public virtual ValueTask WriteMessageAsync(Message message)
{
MessagesWritten++;
return default;
}
public virtual ValueTask WritePostambleAsync() => default;

View File

@@ -12,8 +12,6 @@ namespace DiscordChatExporter.Core.Exporting.Writers
{
private readonly TextWriter _writer;
private long _messageCount;
public PlainTextMessageWriter(Stream stream, ExportContext context)
: base(stream, context)
{
@@ -130,26 +128,29 @@ namespace DiscordChatExporter.Core.Exporting.Writers
public override async ValueTask WriteMessageAsync(Message message)
{
await base.WriteMessageAsync(message);
// Header
await WriteMessageHeaderAsync(message);
// Content
if (!string.IsNullOrWhiteSpace(message.Content))
await _writer.WriteLineAsync(FormatMarkdown(message.Content));
await _writer.WriteLineAsync();
// Attachments, embeds, reactions
await WriteAttachmentsAsync(message.Attachments);
await WriteEmbedsAsync(message.Embeds);
await WriteReactionsAsync(message.Reactions);
await _writer.WriteLineAsync();
_messageCount++;
}
public override async ValueTask WritePostambleAsync()
{
await _writer.WriteLineAsync('='.Repeat(62));
await _writer.WriteLineAsync($"Exported {_messageCount:N0} message(s)");
await _writer.WriteLineAsync($"Exported {MessagesWritten:N0} message(s)");
await _writer.WriteLineAsync('='.Repeat(62));
}