Skip to content

Instantly share code, notes, and snippets.

@jvyden
Created March 15, 2024 20:43
Show Gist options
  • Save jvyden/e1789607b41e40d8cbceff264a2da2ce to your computer and use it in GitHub Desktop.
Save jvyden/e1789607b41e40d8cbceff264a2da2ce to your computer and use it in GitHub Desktop.
using System.Diagnostics;
using Discord;
using Discord.WebSocket;
using Moniku.Core;
using Moniku.Core.Modules;
using Moniku.Data;
namespace Moniku.Modules;
public class VideoCompressorModule : Module {
public VideoCompressorModule(Bot bot) : base(bot) {}
public override void Initialize() {
this.Bot.Client.ReactionAdded += this.clientOnReact;
}
public override Task Ready() {
return Task.CompletedTask;
}
public override GatewayIntents RequiredIntents =>
GatewayIntents.GuildMessageReactions |
GatewayIntents.DirectMessageReactions;
public const int MaxHeight = 720;
public const int DefaultHeight = 480;
public const int MinutesToTimeout = 5;
private const int maximumAmountOfCompressTries = 4;
/// <summary>
/// A list of recognized video extensions.
/// </summary>
/// <remarks>
/// Should be kept in order of most common to least common.
/// </remarks>
private static readonly List<string> videoExtensions = new() {
"mp4",
"mov",
"webm",
"mkv",
"flv",
"avi",
};
/// <summary>
/// The extension compressed videos will end up as.
/// </summary>
private const string videoFinalExtension = "mp4";
internal static CancellationToken Token => new CancellationTokenSource(TimeSpan.FromMinutes(MinutesToTimeout)).Token;
public override bool IsModuleCompatible {
get {
Process process = Process.Start("ffmpeg", "-version");
process.WaitForExit(10000);
return process.HasExited && process.ExitCode == 0;
}
}
private static string getOutputName(string filename) {
return $"{Path.GetFileNameWithoutExtension(filename)}-compressed{Path.GetExtension(filename)}";
}
private static string getExtension(string path) {
string currentExtension = Path.GetExtension(path);
// substr removes . at start of extension
if(videoExtensions.Contains(currentExtension.Substring(1))) {
return "." + videoFinalExtension;
}
return currentExtension;
}
private async Task clientOnReact(Cacheable<IUserMessage, ulong> messageCache, Cacheable<IMessageChannel, ulong> __, SocketReaction reaction) {
if(reaction.Emote is not Emote emote) return;
if(emote.Id != this.Bot.Config.EmojiSettings.CompressEmoji) return;
IUserMessage message = await messageCache.GetOrDownloadAsync();
try {
await this.compressFromMessage(message, Token);
}
catch(Exception e) {
await message.ReplyAsync($"what is this bullshit\n```csharp\n{e}```");
}
}
private async Task compressFromMessage(IMessage message, CancellationToken ct) {
#region Attachments
using HttpClient httpClient = new();
foreach(IAttachment attachment in message.Attachments) {
byte[] data = await httpClient.GetByteArrayAsync(attachment.Url, ct);
// Start a new thread to compress in.
await Task.Factory.StartNew(async () => {
try {
await this.compressAndUpload(message, attachment.Filename, attachment.IsSpoiler(), attachment.Description, data, ct);
}
catch(Exception e) {
MessageReference reference = new(message.Id);
await message.Channel.SendMessageAsync(
text: $"what is this bullshit\n```csharp\n{e}```",
messageReference: reference
);
}
}, ct);
}
#endregion
#region URLs
string[] split = message.CleanContent.Split(' ', '\n');
// Detect URLs by trying to create one for each word in the message.
foreach(string w in split) {
bool spoiler = w.StartsWith("||") && w.EndsWith("||");
string word = spoiler ? w.Replace("||", "") : w;
if(!Uri.TryCreate(word, UriKind.Absolute, out Uri? url)) continue;
if(url.Scheme != "https" && url.Scheme != "http") continue;
// Tenor requires special handling.
byte[]? data = null;
if(url.Host == "tenor.com") {
int dashIndex = url.LocalPath.LastIndexOf('-');
string idStr = url.LocalPath.Substring(dashIndex + 1, url.LocalPath.Length - dashIndex - 1);
Console.WriteLine(idStr);
if(!int.TryParse(idStr, out int id)) continue;
TenorModule tenor = Bot.GetModule<TenorModule>();
data = await tenor.DownloadGif(id);
url = new Uri(url + ".gif");
}
else {
// Download URL contents
data = await httpClient.GetByteArrayAsync(url, ct);
}
if(data == null) continue;
// Start a new thread to compress in.
await Task.Factory.StartNew(async () => {
try {
await this.compressAndUpload(message, url.LocalPath, spoiler, "", data, ct);
}
catch(Exception e) {
MessageReference reference = new(message.Id);
await message.Channel.SendMessageAsync(
text: $"what is this bullshit\n```csharp\n{e}```",
messageReference: reference
);
}
}, ct);
}
#endregion
}
private async Task compressAndUpload(IMessage message, string attachmentName, bool spoiler, string description, byte[] data, CancellationToken ct) {
using IDisposable typingState = message.Channel.EnterTypingState();
CompressedVideo video = await this.Compress(attachmentName, spoiler, description, data, ct);
await message.Channel.SendFileAsync(
attachment: video.File,
messageReference: new MessageReference(message.Id),
text: $"Compressing this file saved {Math.Round(video.OldSize - video.NewSize, 2)}MB~"
);
Console.WriteLine("done");
}
public async Task<CompressedVideo> Compress(string attachmentName, bool spoiler, string description, byte[] data, CancellationToken ct, int height = DefaultHeight, int attempt = 0) {
string newExtension = getExtension(attachmentName);
string uncompressedPath = Path.GetTempFileName() + Path.GetExtension(attachmentName);
string compressedPath = Path.GetTempFileName() + newExtension;
await File.WriteAllBytesAsync(uncompressedPath, data, ct);
attachmentName = Path.ChangeExtension(attachmentName, newExtension);
height = Math.Min(height, MaxHeight);
height -= height % 2;
string videoFlags = $"-vf scale=-2:{height} -r 30 " +
"-c:v libx264 -preset veryslow -crf 27";
string audioFlags = "-c:a aac -q:a 2";
Process process = new() {
StartInfo = new ProcessStartInfo {
FileName = "ffmpeg",
Arguments = $"-i {uncompressedPath} {videoFlags} {audioFlags} -movflags +faststart -y {compressedPath}",
},
};
process.Start();
await process.WaitForExitAsync(ct);
File.Delete(uncompressedPath);
byte[] compressedData = await File.ReadAllBytesAsync(compressedPath, ct);
const int maxFileSize = 8 * 1_048_576;
if((compressedData.Length > maxFileSize || compressedData.Length >= data.Length) && attempt < maximumAmountOfCompressTries) {
// TODO: tell user file is too big and we are trying again
Console.WriteLine("File too big; compressing again");
return await this.Compress(attachmentName, spoiler, description, data, ct, height / 2, attempt + 1);
}
// TODO: add nullable support for this function
// if(compressedData.Length == 0) {
// return null;
// }
FileAttachment attachment = new(new MemoryStream(compressedData), getOutputName(attachmentName)) {
IsSpoiler = spoiler,
Description = description,
};
File.Delete(compressedPath);
return new CompressedVideo {
File = attachment,
NewSize = compressedData.Length / 1_048_576f,
OldSize = data.Length / 1_048_576f,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment