Haha-Yes/commands/utility/download.js

326 lines
11 KiB
JavaScript
Raw Normal View History

2023-04-10 14:48:47 +02:00
import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js';
import { execFile } from 'node:child_process';
2022-06-17 01:25:05 +02:00
import fs from 'node:fs';
import os from 'node:os';
2022-06-17 07:31:58 +02:00
import utils from '../../utils/videos.js';
2022-06-17 01:25:05 +02:00
let client;
let maxFileSize;
2024-01-26 18:23:09 +01:00
let { ytdlpMaxResolution } = process.env;
// Convert to number as process.env is always a string
ytdlpMaxResolution = Number(ytdlpMaxResolution);
2022-06-17 01:25:05 +02:00
export default {
data: new SlashCommandBuilder()
.setName('download')
.setDescription('Download a video.')
.addStringOption(option =>
option.setName('url')
.setDescription('url of the video you want to download.')
.setRequired(true))
.addBooleanOption(option =>
2022-08-22 20:23:27 +02:00
option.setName('format')
.setDescription('Choose the quality of the video.')
2022-08-22 20:23:27 +02:00
.setRequired(false))
.addBooleanOption(option =>
option.setName('compress')
.setDescription('Compress the video.')
.setRequired(false))
.addBooleanOption(option =>
option.setName('autocrop')
.setDescription('Autocrop borders on videos. Ignored when using compress option.')
2024-03-05 19:41:31 +01:00
.setRequired(false))
.addBooleanOption(option =>
option.setName('description')
.setDescription('Include the video description.')
.setRequired(false)),
category: 'utility',
2022-08-30 23:29:57 +02:00
alias: ['dl'],
integration_types: [0, 1],
async execute(interaction, args, c) {
client = c;
2022-09-01 01:43:59 +02:00
const url = args.url;
const format = args.format;
maxFileSize = await utils.getMaxFileSize(interaction.guild);
2022-09-01 01:43:59 +02:00
interaction.doCompress = args.compress;
interaction.doAutocrop = args.autocrop;
await interaction.deferReply({ ephemeral: false });
if (interaction.isMessage) {
interaction.delete();
}
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 });
2022-06-16 23:11:01 +02:00
}
if (format) {
let qualitys = await new Promise((resolve, reject) => {
execFile('./bin/yt-dlp', [url, '--print', '%()j'], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout);
});
});
2023-04-20 19:54:21 +02:00
qualitys = JSON.parse(qualitys);
const options = [];
qualitys.formats.forEach(f => {
if (f.format.includes('storyboard')) return;
options.push({
2022-07-06 18:52:44 +02:00
label: f.resolution ? f.resolution : 'Unknown format',
2022-06-16 14:28:49 +02:00
description: `${f.format} V: ${f.vcodec} A: ${f.acodec}`,
value: f.format_id,
});
});
2022-07-06 18:52:44 +02:00
if (options.length < 2) {
await interaction.deleteReply();
return interaction.followUp({ content: '❌ There is no other quality option for this video!', ephemeral: true });
}
if (options.length > 25) {
// Reverse so the higher quality formats are first
options.reverse();
while (options.length > 25) {
// Remove the lower quality formats
options.pop();
}
// Reverse again so the lower quality appears first
options.reverse();
}
const row = new ActionRowBuilder()
.addComponents(
2023-04-10 14:48:47 +02:00
new StringSelectMenuBuilder()
.setCustomId(`downloadQuality${interaction.user.id}${interaction.id}`)
.setPlaceholder('Nothing selected')
2022-06-16 14:36:38 +02:00
.setMinValues(1)
.setMaxValues(2)
.addOptions(options),
);
await interaction.deleteReply();
await interaction.followUp({ content: 'Which quality do you want?', ephemeral: true, components: [row] });
client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isSelectMenu()) return;
2023-04-10 14:48:47 +02:00
if (interactionMenu.customId === `downloadQuality${interaction.user.id}${interaction.id}`) {
await interactionMenu.deferReply({ ephemeral: false });
2023-04-20 19:54:21 +02:00
2024-01-26 18:23:09 +01:00
await checkSize(url, interactionMenu.values[0], args, interaction);
2024-03-05 19:41:31 +01:00
return download(url, interactionMenu, interaction, undefined, true);
}
});
return;
}
2024-01-26 18:23:09 +01:00
const newFormat = await checkSize(url, undefined, args, interaction);
2024-03-05 19:41:31 +01:00
return download(url, interaction, interaction, newFormat, args.description);
},
};
2024-03-05 19:41:31 +01:00
async function download(url, interaction, originalInteraction, format = undefined, description = false) {
2024-06-12 00:30:34 +02:00
let embedColour = 'Navy';
if (interaction.member) {
if (interaction.member.displayHexColor) {
embedColour = interaction.member.displayHexColor;
}
}
const Embed = new EmbedBuilder()
2024-06-12 00:30:34 +02:00
.setColor(embedColour)
2023-09-13 20:56:06 +02:00
.setAuthor({ name: `Downloaded by ${interaction.user.username}`, iconURL: interaction.user.displayAvatarURL(), url: url })
.setFooter({ text: `You can get the original video by clicking on the "Downloaded by ${interaction.user.username}" message!` });
2024-03-05 19:41:31 +01:00
if (description) {
Embed.setDescription(await getVideoDescription(url));
}
2024-01-26 18:23:09 +01:00
if (interaction.customId === `downloadQuality${interaction.user.id}${originalInteraction.id}` && !format) {
format = interaction.values[0];
2022-06-16 14:36:38 +02:00
if (interaction.values[1]) format += '+' + interaction.values[1];
}
2022-06-17 01:25:05 +02:00
utils.downloadVideo(url, interaction.id, format)
.then(async () => {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
let output = `${os.tmpdir()}/${file}`;
2022-08-22 20:23:27 +02:00
const compressInteraction = originalInteraction ? originalInteraction : interaction;
if (compressInteraction.doCompress) {
2024-07-04 05:53:54 +02:00
const presets = [ 'Social 25 MB 5 Minutes 360p60', 'Social 25 MB 2 Minutes 540p60', 'Social 25 MB 1 Minute 720p60', 'Social 25 MB 30 Seconds 1080p60' ];
2022-08-22 20:23:27 +02:00
const options = [];
presets.forEach(p => {
options.push({
label: p,
value: p,
});
});
const row = new ActionRowBuilder()
2022-08-22 20:23:27 +02:00
.addComponents(
2023-04-10 14:48:47 +02:00
new StringSelectMenuBuilder()
.setCustomId(`preset${interaction.user.id}${interaction.id}`)
2022-08-22 20:23:27 +02:00
.setPlaceholder('Nothing selected')
.addOptions(options),
);
await interaction.deleteReply();
await interaction.followUp({ content: 'Which compression preset do you want?', ephemeral: true, components: [row] });
client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return;
2022-08-22 20:23:27 +02:00
if (!interactionMenu.isSelectMenu()) return;
2023-04-10 14:48:47 +02:00
if (interactionMenu.customId === `preset${interaction.user.id}${interaction.id}`) {
2022-08-22 20:23:27 +02:00
await interactionMenu.deferReply({ ephemeral: false });
compress(file, interactionMenu, Embed);
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
2022-08-22 20:23:27 +02:00
}
});
return;
}
// If the video format is not one compatible with Discord, reencode it unless autocrop is choosen in which case it gets reencoded anyway.
/* Update: Discord now support hevc
if (!interaction.doAutocrop) {
const bannedFormats = ['hevc'];
const codec = await utils.getVideoCodec(output);
2024-07-11 07:19:25 +02:00
if (bannedFormats.includes(codec)) {
const oldOutput = output;
output = `${os.tmpdir()}/264${file}`;
await utils.ffmpeg(['-i', oldOutput, '-vcodec', 'libx264', '-acodec', 'aac', output]);
}
}
else
*/
if (interaction.doAutocrop && !compressInteraction.doCompress) {
const oldOutput = output;
output = `${os.tmpdir()}/autocrop${file}`;
await utils.autoCrop(oldOutput, output);
}
const fileStat = fs.statSync(output);
const fileSize = fileStat.size / 1000000.0;
Embed.setAuthor({ name: `${Embed.data.author.name} (${fileSize.toFixed(2)} MB)`, iconURL: Embed.data.author.icon_url, url: Embed.data.author.url });
2024-01-26 18:23:09 +01:00
let message = null;
if (interaction.isMessage && interaction.reference !== null) {
const channel = client.channels.resolve(interaction.reference.channelId);
message = await channel.messages.fetch(interaction.reference.messageId);
}
if (fileSize > 100) {
await interaction.deleteReply();
await interaction.followUp('Uh oh! The video you tried to download is too big!', { ephemeral: true });
}
else if (fileSize > maxFileSize) {
const fileurl = await utils.upload(output)
.catch(err => {
console.error(err);
});
2024-01-26 18:23:09 +01:00
await interaction.editReply({ content: `File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.`, embeds: [Embed], ephemeral: false });
2024-01-26 18:33:57 +01:00
if (interaction.isMessage && message) {
2024-01-26 18:23:09 +01:00
await message.reply({ content: fileurl });
}
else {
await interaction.followUp({ content: fileurl, ephemeral: false });
}
}
2024-01-26 18:33:57 +01:00
else if (interaction.isMessage && message) {
2024-01-26 18:23:09 +01:00
await message.reply({ embeds: [Embed], files: [output] });
}
else {
await interaction.editReply({ embeds: [Embed], files: [output], ephemeral: false });
}
2024-01-26 18:33:57 +01:00
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
})
.catch(async err => {
console.error(err);
await interaction.deleteReply();
await interaction.followUp({ content: 'Uh oh! An error has occured!', ephemeral: true });
});
return;
}
2022-08-22 20:23:27 +02:00
async function compress(input, interaction, embed) {
const output = `compressed${input}.mp4`;
// Delete the file as it apparently don't overwrite?
if (fs.existsSync(output)) {
fs.rmSync(output);
}
2022-08-22 20:23:27 +02:00
utils.compressVideo(`${os.tmpdir()}/${input}`, output, interaction.values[0])
.then(async () => {
const fileStat = fs.statSync(`${os.tmpdir()}/${output}`);
const fileSize = fileStat.size / 1000000.0;
embed.setAuthor({ name: `${embed.data.author.name} (${fileSize.toFixed(2)} MB)`, iconURL: embed.data.author.icon_url, url: embed.data.author.url });
if (fileSize > maxFileSize) {
await interaction.editReply({ content: `File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.`, ephemeral: false });
2022-08-22 20:23:27 +02:00
}
else {
await interaction.editReply({ embeds: [embed], files: [`${os.tmpdir()}/${output}`], ephemeral: false });
}
});
2024-01-26 18:23:09 +01:00
}
async function checkSize(url, format, args, interaction, tries = 0) {
const resolutions = [144, 240, 360, 480, 720, 1080, 1440, 2160];
while (tries < 4) {
format = `bestvideo[height<=?${resolutions[resolutions.indexOf(ytdlpMaxResolution) - tries]}]+bestaudio/best`;
const aproxFileSize = await utils.getVideoSize(url, format);
2024-01-26 18:33:57 +01:00
if (isNaN(aproxFileSize)) return format;
2024-01-26 18:23:09 +01:00
if (format || tries >= 4) {
if (aproxFileSize > 100 && !args.compress && tries > 4) {
return await interaction.followUp(`Uh oh! The video you tried to download is larger than 100 mb (is ${aproxFileSize} mb)! Try again with a lower resolution format.`);
}
else if (aproxFileSize > 500 && tries > 4) {
return await interaction.followUp(`Uh oh! The video you tried to download is larger than 500 mb (is ${aproxFileSize} mb)! Try again with a lower resolution format.`);
}
}
if (aproxFileSize < 100) {
return format;
}
if (tries < 4 && aproxFileSize > 100) {
tries++;
}
}
2024-03-05 19:41:31 +01:00
}
async function getVideoDescription(urlArg) {
return await new Promise((resolve, reject) => {
execFile('./bin/yt-dlp', [urlArg, '--no-warnings', '-O', '%(description)s'], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout.slice(0, 240));
});
});
2022-08-22 20:23:27 +02:00
}