forked from Supositware/Haha-Yes
166 lines
No EOL
5.1 KiB
JavaScript
166 lines
No EOL
5.1 KiB
JavaScript
import { SlashCommandBuilder } from 'discord.js';
|
||
import utils from '../../utils/videos.js';
|
||
import fs from 'node:fs';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { execFile } from 'node:child_process';
|
||
const { NODE_ENV } = process.env;
|
||
const ytdlpFormat = 'bestvideo[height<=?480]/best';
|
||
|
||
export default {
|
||
data: new SlashCommandBuilder()
|
||
.setName('vid2gif')
|
||
.setDescription('Convert your video into a gif.')
|
||
.addStringOption(option =>
|
||
option.setName('url')
|
||
.setDescription('URL of the video you want to convert')
|
||
.setRequired(true))
|
||
.addIntegerOption(option =>
|
||
option.setName('quality')
|
||
.setDescription('Quality of the gif conversion. Default 70. Number between 1 and 100')
|
||
.setRequired(false))
|
||
.addIntegerOption(option =>
|
||
option.setName('fps')
|
||
.setDescription('Change the speed at which the gif play at. Number between 1 and 100.')
|
||
.setRequired(false))
|
||
.addBooleanOption(option =>
|
||
option.setName('autocrop')
|
||
.setDescription('Autocrop borders on gif.')
|
||
.setRequired(false))
|
||
.addBooleanOption(option =>
|
||
option.setName('noloop')
|
||
.setDescription('Stop the gif from looping')
|
||
.setRequired(false)),
|
||
category: 'utility',
|
||
alias: ['v2g', 'togif'],
|
||
integration_types: [0, 1],
|
||
|
||
async execute(interaction, args) {
|
||
await interaction.deferReply({ ephemeral: false });
|
||
const maxFileSize = await utils.getMaxFileSize(interaction.guild);
|
||
const url = args.url;
|
||
let quality = args.quality;
|
||
if (quality) {
|
||
if (quality <= 0) {
|
||
quality = 1;
|
||
}
|
||
else if (quality > 100) {
|
||
quality = 100;
|
||
}
|
||
}
|
||
|
||
if (args.fps) {
|
||
if (args.fps <= 0) {
|
||
args.fps = 1;
|
||
}
|
||
else if (args.fps > 100) {
|
||
args.fps = 100;
|
||
}
|
||
}
|
||
|
||
if (!await utils.stringIsAValidurl(url)) {
|
||
console.error(`Not a url!!! ${url}`);
|
||
return interaction.editReply({ content: '❌ This does not look like a valid url!', ephemeral: true });
|
||
}
|
||
|
||
const aproxFileSize = await utils.getVideoSize(url, ytdlpFormat);
|
||
console.log(aproxFileSize);
|
||
|
||
if (aproxFileSize > 4) {
|
||
return interaction.editReply('The file you are trying to convert is too big! Limit is 4 MB');
|
||
};
|
||
|
||
utils.downloadVideo(url, interaction.id, ytdlpFormat)
|
||
.then(async () => {
|
||
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
|
||
let output = `${os.tmpdir()}/${file}`;
|
||
|
||
if (args.autocrop) {
|
||
const oldOutput = output;
|
||
output = `${os.tmpdir()}/autocrop${file}`;
|
||
await utils.autoCrop(oldOutput, output);
|
||
}
|
||
|
||
// Get the fps of the original video
|
||
if (!args.fps) {
|
||
args.fps = getVideoFramerate(output);
|
||
}
|
||
|
||
const gifskiOutput = output.replace(path.extname(output), '.gif');
|
||
const gifsicleOutput = output.replace(path.extname(output), 'gifsicle.gif');
|
||
|
||
// Extract every frame for gifski
|
||
await utils.ffmpeg(['-i', output, `${os.tmpdir()}/frame${interaction.id}%04d.png`]);
|
||
// Make it look better
|
||
await gifski(gifskiOutput, `${os.tmpdir()}/frame${interaction.id}*`, quality, await args.fps);
|
||
// Optimize it
|
||
await gifsicle(gifskiOutput, gifsicleOutput, args.noloop);
|
||
|
||
const fileStat = fs.statSync(gifsicleOutput);
|
||
const fileSize = fileStat.size / 1000000.0;
|
||
|
||
if (fileSize > 25) {
|
||
await interaction.deleteReply();
|
||
await interaction.followUp('❌ Uh oh! The video once converted is too big!', { ephemeral: true });
|
||
}
|
||
else if (fileSize > maxFileSize) {
|
||
const fileURL = await utils.upload(gifsicleOutput)
|
||
.catch(err => {
|
||
console.error(err);
|
||
});
|
||
await interaction.editReply({ content: `ℹ️ File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.\n${fileURL}`, ephemeral: false });
|
||
}
|
||
else {
|
||
await interaction.editReply({ files: [gifsicleOutput], ephemeral: false });
|
||
}
|
||
});
|
||
},
|
||
};
|
||
|
||
async function gifski(output, input, quality, fps) {
|
||
return await new Promise((resolve, reject) => {
|
||
// Shell: true should be fine as no user input is being passed
|
||
execFile('gifski', ['--quality', quality ? quality : 70, '--fps', fps ? fps : 20, '-o', output, input], { shell: true }, (err, stdout, stderr) => {
|
||
if (err) {
|
||
reject(stderr);
|
||
}
|
||
if (stderr) {
|
||
console.error(stderr);
|
||
}
|
||
console.log(NODE_ENV === 'development' ? stdout : null);
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
async function gifsicle(input, output, loop = false) {
|
||
return await new Promise((resolve, reject) => {
|
||
// Shell: true should be fine as no user input is being passed
|
||
execFile('gifsicle', ['--colors', '256', loop ? '--no-loopcount' : '', '-i', input, '-o', output], { shell: true }, (err, stdout, stderr) => {
|
||
if (err) {
|
||
reject(stderr);
|
||
}
|
||
if (stderr) {
|
||
console.error(stderr);
|
||
}
|
||
console.log(NODE_ENV === 'development' ? stdout : null);
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
async function getVideoFramerate(input) {
|
||
return await new Promise((resolve, reject) => {
|
||
execFile('ffprobe', ['-v', 'error', '-of', 'csv=p=0', '-select_streams', 'v:0', '-show_entries', 'stream=r_frame_rate', input], (err, stdout, stderr) => {
|
||
if (err) {
|
||
reject(stderr);
|
||
}
|
||
if (stderr) {
|
||
console.error(stderr);
|
||
}
|
||
const tempfps = stdout.trim().split('/');
|
||
const fps = tempfps[0] / tempfps[1];
|
||
return resolve(fps);
|
||
});
|
||
});
|
||
} |