-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Port VideoUtils to InteractionFramework
- Loading branch information
Showing
2 changed files
with
243 additions
and
148 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
using Discord; | ||
using Discord.Interactions; | ||
using Discord.WebSocket; | ||
using FFMpegCore; | ||
using FFMpegCore.Extend; | ||
using System.Net.Mail; | ||
using System.Runtime.InteropServices; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace QuickEdit.Commands.Modules; | ||
[Group("video", "Video utilities")] | ||
[IntegrationType(ApplicationIntegrationType.UserInstall)] | ||
[CommandContextType(InteractionContextType.Guild, InteractionContextType.PrivateChannel)] | ||
public class VideoUtils : InteractionModuleBase | ||
{ | ||
[SlashCommand("trim", "Trim a video")] | ||
public async Task TrimVideoAsync( | ||
[Summary(description: "The video to trim")] Discord.Attachment video, | ||
[Summary("start", "What time should the video start? [XXh XXm XXs XXms]")] string trimStartString = "", | ||
[Summary("end", "What time should the video end? [XXh XXm XXs XXms]")] string trimEndString = "", | ||
[Summary(description: "A message to send with the video when it's trimmed")] string message = "", | ||
[Summary(description: "If the video should be sent as a temporary message, that's only visible to you")] bool ephemeral = false) | ||
{ | ||
string videoInputPath = "./tmp/input.mp4"; // Normally, I would use Path.GetTempFileName(), but FFMpegCore doesn't seem to | ||
string videoOutputPath = "./tmp/output.mp4"; // like the .tmp file extension (or anything other than .mp4) as far as i know | ||
|
||
// Achknowledge the command | ||
await DeferAsync(ephemeral); | ||
|
||
// There is a similar check for the TimeSpan library below, but this it to avoid | ||
if (trimStartString == "" && trimEndString == "") | ||
{ | ||
await FollowupAsync("You must provide a start or end time to trim the video.", ephemeral: true); | ||
return; | ||
} | ||
|
||
// Reject incorrect video formats | ||
if (video.ContentType != "video/mp4") | ||
{ | ||
await FollowupAsync("Invalid video format. Please provide an MP4 file.", ephemeral: true); | ||
return; | ||
} | ||
|
||
// Get TimeSpans | ||
TimeSpan trimStart; | ||
TimeSpan trimEnd; | ||
try { | ||
if (trimStartString != "") // Avoid invalid format exceptions | ||
trimStart = TimeSpanFromHMS(trimStartString); | ||
else | ||
trimStart = TimeSpan.Zero; | ||
|
||
if (trimEndString != "") // Avoid invalid format exceptions | ||
trimEnd = TimeSpanFromHMS(trimEndString); | ||
else | ||
trimEnd = TimeSpan.Zero; | ||
} catch (Exception e) { | ||
if (e is ArgumentException) | ||
{ | ||
await FollowupAsync("Invalid time format. Please provide a valid time format (XXh XXm XXs XXms).", ephemeral: true); | ||
} else { | ||
throw; | ||
} | ||
return; | ||
} | ||
|
||
// Make sure the times are not negative | https://stackoverflow.com/a/1018659/17003609 (comment) | ||
trimStart = trimStart.Duration(); | ||
trimEnd = trimEnd.Duration(); | ||
|
||
// The video can't be trimmed if both start and end times are 0 | ||
if (trimStart == TimeSpan.Zero && trimEnd == TimeSpan.Zero) | ||
{ | ||
await FollowupAsync("You must provide a start or end time to trim the video.", ephemeral: true); | ||
return; | ||
} | ||
|
||
await DownloadVideoAsync(video.Url, videoInputPath); | ||
|
||
var mediaInfo = await FFProbe.AnalyseAsync(videoInputPath); | ||
CheckTimes(ref trimStart, ref trimEnd, mediaInfo.Duration); | ||
|
||
// Check if the temporary directory, where the video is supposed to be exists | ||
if (!Directory.Exists("./tmp")) | ||
{ | ||
Directory.CreateDirectory("./tmp"); | ||
} | ||
|
||
// Process and send video | ||
await FFMpeg.SubVideoAsync(videoInputPath, videoOutputPath, (TimeSpan)trimStart, (TimeSpan)trimEnd); // Need to convert the TimeSpans since the value is nullable | ||
await FollowupWithFileAsync(videoOutputPath, video.Filename, message, ephemeral: ephemeral); | ||
|
||
// Clean up | ||
File.Delete(videoInputPath); | ||
File.Delete(videoOutputPath); | ||
} | ||
|
||
/// <summary> | ||
/// Check and set the start and end times to follow restrictions: | ||
/// <list type="bullet"> | ||
/// <item>Set <paramref name="trimEnd"/> to <paramref name="duration"/> if smaller or equal to <paramref name="trimStart"/></item> | ||
/// <item>Clamp <paramref name="trimEnd"/> to the <paramref name="duration"/></item> | ||
/// <item>Set <paramref name="trimStart"/> to <c>0</c> if it's greater or equal to the video's <paramref name="duration"/></item> | ||
/// </list> | ||
/// </summary> | ||
/// <param name="trimStart">Start of the trim</param> | ||
/// <param name="trimEnd">End of the trim</param> | ||
/// <param name="duration">Duration of the video</param> | ||
private static void CheckTimes(ref TimeSpan trimStart, ref TimeSpan trimEnd, TimeSpan duration) | ||
{ | ||
// Set trimEnd to duration if smaller or equal to trimStart | ||
if (trimEnd <= trimStart) | ||
{ | ||
trimEnd = duration; | ||
} | ||
|
||
// Clamp the end time to the video's duration | ||
trimEnd = new[] { duration, trimEnd }.Min(); // https://stackoverflow.com/a/1985326/17003609 | ||
|
||
// Set trimStart to 0 if it's greater or equal to the video's duration | ||
if (trimStart >= duration) | ||
{ | ||
trimStart = TimeSpan.Zero; | ||
} | ||
} | ||
|
||
private static async Task DownloadVideoAsync(string uri, string path) | ||
{ | ||
using var client = new HttpClient(); | ||
using var s = await client.GetStreamAsync(uri); | ||
using var fs = new FileStream(path, FileMode.OpenOrCreate); | ||
await s.CopyToAsync(fs); | ||
fs.Close(); | ||
} | ||
|
||
/// <summary> | ||
/// Parses a string in the format 'XXh XXm XXs XXms' into a TimeSpan object | ||
/// </summary> | ||
/// <param name="input">Input string to parse, in format [XXh XXm XXs]</param> | ||
/// <returns>The parsed TimeSpan</returns> | ||
/// <exception cref="ArgumentException">Thrown when the input string is in an invalid format</exception> | ||
public static TimeSpan TimeSpanFromHMS(string input) | ||
{ | ||
if (string.IsNullOrWhiteSpace(input)) | ||
{ | ||
throw new ArgumentException("Input string is not in a valid format"); | ||
} | ||
|
||
// Define the regular expression pattern to match hours, minutes, and seconds | ||
string pattern = @"((?<hours>\d+)h)?\s*((?<minutes>\d+)m|min)?\s*((?<seconds>\d+)s)?\s*((?<milliseconds>\d+)ms)?"; | ||
|
||
// Match the input string with the pattern | ||
var match = Regex.Match(input, pattern, RegexOptions.IgnoreCase); | ||
|
||
// Check if at least one component (hours, minutes, or seconds) is present | ||
if (!match.Groups["hours"].Success && !match.Groups["minutes"].Success && !match.Groups["seconds"].Success && !match.Groups["milliseconds"].Success) | ||
{ | ||
throw new ArgumentException("Input string is not in a valid format"); | ||
} | ||
|
||
// Extract the matched groups | ||
int hours = 0; | ||
if (match.Groups["hours"].Success) int.TryParse(match.Groups["hours"].Value, out hours); | ||
int minutes = 0; | ||
if (match.Groups["minutes"].Success) int.TryParse(match.Groups["minutes"].Value, out minutes); | ||
int seconds = 0; | ||
if (match.Groups["seconds"].Success) int.TryParse(match.Groups["seconds"].Value, out seconds); | ||
int milliseconds = 0; | ||
if (match.Groups["milliseconds"].Success) int.TryParse(match.Groups["milliseconds"].Value, out milliseconds); | ||
|
||
// Create and return the TimeSpan object | ||
return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); | ||
} | ||
|
||
/* | ||
private static async Task DownloadVideoAsync(string uri, string path) | ||
{ | ||
using var client = new HttpClient(); | ||
using var s = await client.GetStreamAsync(uri); | ||
using var fs = new FileStream(path, FileMode.OpenOrCreate); | ||
await s.CopyToAsync(fs); | ||
fs.Close(); | ||
} | ||
private static async Task<TimeSpan?> GetTrimTimeAsync(string? timeString, SocketSlashCommand command) | ||
{ | ||
if (timeString == null) | ||
{ | ||
// This will later be replaced with the video's duration | ||
return TimeSpan.Zero; | ||
} | ||
try | ||
{ | ||
return TimeSpanFromHMS(timeString); | ||
} | ||
catch | ||
{ | ||
await command.FollowupAsync("Invalid time format. Please provide a valid time format (XXh XXm XXs XXms).", ephemeral: true); | ||
await Program.LogAsync("VideoUtils", $"Invalid time format in TrimVideoAsync (received: {timeString})", LogSeverity.Verbose); | ||
return null; | ||
} | ||
} | ||
/// <summary> | ||
/// Parses a string in the format 'XXh XXm XXs XXms' into a TimeSpan object | ||
/// </summary> | ||
/// <param name="input">Input string to parse, in format [XXh XXm XXs]</param> | ||
/// <returns>The parsed TimeSpan</returns> | ||
/// <exception cref="ArgumentException">Thrown when the input string is in an invalid format</exception> | ||
public static TimeSpan TimeSpanFromHMS(string input) | ||
{ | ||
if (string.IsNullOrWhiteSpace(input)) | ||
{ | ||
throw new ArgumentException("Input string is not in a valid format"); | ||
} | ||
// Define the regular expression pattern to match hours, minutes, and seconds | ||
string pattern = @"((?<milliseconds>\d+)ms)?\s*((?<hours>\d+)h)?\s*((?<minutes>\d+)m|min)?\s*((?<seconds>\d+)s)?"; | ||
// Match the input string with the pattern | ||
var match = Regex.Match(input, pattern, RegexOptions.IgnoreCase); | ||
// Check if at least one component (hours, minutes, or seconds) is present | ||
if (!match.Groups["hours"].Success && !match.Groups["minutes"].Success && !match.Groups["seconds"].Success && !match.Groups["milliseconds"].Success) | ||
{ | ||
throw new ArgumentException("Input string is not in a valid format"); | ||
} | ||
// Extract the matched groups | ||
int hours = 0; | ||
if (match.Groups["hours"].Success) int.TryParse(match.Groups["hours"].Value, out hours); | ||
int minutes = 0; | ||
if (match.Groups["minutes"].Success) int.TryParse(match.Groups["minutes"].Value, out minutes); | ||
int seconds = 0; | ||
if (match.Groups["seconds"].Success) int.TryParse(match.Groups["seconds"].Value, out seconds); | ||
int milliseconds = 0; | ||
if (match.Groups["milliseconds"].Success) int.TryParse(match.Groups["milliseconds"].Value, out milliseconds); | ||
// Create and return the TimeSpan object | ||
return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); | ||
}*/ | ||
} |
This file was deleted.
Oops, something went wrong.