Created
March 15, 2024 20:43
-
-
Save jvyden/e1789607b41e40d8cbceff264a2da2ce to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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