2023-04-10 14:48:47 +02:00
import { SlashCommandBuilder , EmbedBuilder , ActionRowBuilder , StringSelectMenuBuilder } from 'discord.js' ;
2024-02-02 03:28:46 +01:00
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
2022-08-28 17:03:15 +02:00
let client ;
2023-04-11 20:31:25 +02:00
let maxFileSize ;
2022-08-28 17:03:15 +02:00
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 {
2022-06-16 09:18:39 +02:00
data : new SlashCommandBuilder ( )
. setName ( 'download' )
. setDescription ( 'Download a video.' )
. addStringOption ( option =>
option . setName ( 'url' )
2022-06-20 08:34:18 +02:00
. setDescription ( 'url of the video you want to download.' )
2022-06-16 09:18:39 +02:00
. setRequired ( true ) )
. addBooleanOption ( option =>
2022-08-22 20:23:27 +02:00
option . setName ( 'format' )
2022-06-16 09:18:39 +02:00
. 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?' )
2024-03-05 19:41:31 +01:00
. setRequired ( false ) )
. addBooleanOption ( option =>
option . setName ( 'description' )
. setDescription ( 'Include the video description?' )
2022-06-16 09:18:39 +02:00
. setRequired ( false ) ) ,
2022-08-28 17:03:15 +02:00
category : 'utility' ,
2022-08-30 23:29:57 +02:00
alias : [ 'dl' ] ,
2022-08-28 17:03:15 +02:00
async execute ( interaction , args , c ) {
client = c ;
2022-09-01 01:43:59 +02:00
const url = args . url ;
const format = args . format ;
2023-04-11 20:31:25 +02:00
maxFileSize = await utils . getMaxFileSize ( interaction . guild ) ;
2022-09-01 01:43:59 +02:00
interaction . doCompress = args . compress ;
2023-04-11 20:31:25 +02:00
2022-06-16 09:18:39 +02:00
await interaction . deferReply ( { ephemeral : false } ) ;
2022-08-28 17:03:15 +02:00
if ( interaction . isMessage ) {
interaction . delete ( ) ;
}
2022-06-16 09:18:39 +02:00
2022-06-20 08:34:18 +02:00
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
}
2022-08-28 17:03:15 +02:00
if ( format ) {
2022-06-16 09:18:39 +02:00
let qualitys = await new Promise ( ( resolve , reject ) => {
2024-02-02 03:28:46 +01:00
execFile ( './bin/yt-dlp' , [ url , '--print' , '%()j' ] , ( err , stdout , stderr ) => {
2022-06-16 09:18:39 +02:00
if ( err ) {
reject ( stderr ) ;
}
if ( stderr ) {
console . error ( stderr ) ;
}
resolve ( stdout ) ;
} ) ;
} ) ;
2023-04-20 19:54:21 +02:00
qualitys = JSON . parse ( qualitys ) ;
2022-06-16 09:18:39 +02:00
const options = [ ] ;
qualitys . formats . forEach ( f => {
2022-09-10 09:35:52 +02:00
if ( f . format . includes ( 'storyboard' ) ) return ;
2022-06-16 09:18:39 +02:00
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 } ` ,
2022-06-16 09:18:39 +02:00
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 } ) ;
}
2022-06-16 09:18:39 +02:00
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 ( ) ;
}
2022-08-28 17:03:15 +02:00
const row = new ActionRowBuilder ( )
2022-06-16 09:18:39 +02:00
. addComponents (
2023-04-10 14:48:47 +02:00
new StringSelectMenuBuilder ( )
. setCustomId ( ` downloadQuality ${ interaction . user . id } ${ interaction . id } ` )
2022-06-16 09:18:39 +02:00
. setPlaceholder ( 'Nothing selected' )
2022-06-16 14:36:38 +02:00
. setMinValues ( 1 )
. setMaxValues ( 2 )
2022-06-16 09:18:39 +02:00
. addOptions ( options ) ,
) ;
await interaction . deleteReply ( ) ;
await interaction . followUp ( { content : 'Which quality do you want?' , ephemeral : true , components : [ row ] } ) ;
2022-09-10 09:35:52 +02:00
client . on ( 'interactionCreate' , async ( interactionMenu ) => {
if ( interaction . user !== interactionMenu . user ) return ;
2022-06-16 09:18:39 +02:00
if ( ! interactionMenu . isSelectMenu ( ) ) return ;
2023-04-10 14:48:47 +02:00
if ( interactionMenu . customId === ` downloadQuality ${ interaction . user . id } ${ interaction . id } ` ) {
2022-06-16 09:18:39 +02:00
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 ) ;
2022-06-16 09:18:39 +02:00
}
} ) ;
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 ) ;
2022-06-16 09:18:39 +02:00
} ,
} ;
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 ;
}
}
2022-08-28 17:03:15 +02:00
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! ` } ) ;
2022-06-16 09:18:39 +02:00
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 ) {
2022-06-16 09:18:39 +02:00
format = interaction . values [ 0 ] ;
2022-06-16 14:36:38 +02:00
if ( interaction . values [ 1 ] ) format += '+' + interaction . values [ 1 ] ;
2022-06-16 09:18:39 +02:00
}
2022-06-17 01:25:05 +02:00
utils . downloadVideo ( url , interaction . id , format )
2022-06-16 09:18:39 +02:00
. then ( async ( ) => {
const file = fs . readdirSync ( os . tmpdir ( ) ) . filter ( fn => fn . startsWith ( interaction . id ) ) ;
2023-03-20 03:57:52 +01:00
let output = ` ${ os . tmpdir ( ) } / ${ file } ` ;
2022-06-16 09:18:39 +02:00
2022-08-22 20:23:27 +02:00
const compressInteraction = originalInteraction ? originalInteraction : interaction ;
2022-08-28 17:03:15 +02:00
if ( compressInteraction . doCompress ) {
2024-02-02 03:29:56 +01:00
const presets = [ 'Social 8 MB 3 Minutes 360p30' , 'Social 50 MB 10 Minutes 480p30' , 'Social 50 MB 5 Minutes 720p30' , 'Social 100 MB 5 Minutes 1080p30' ] ;
2022-08-22 20:23:27 +02:00
const options = [ ] ;
presets . forEach ( p => {
options . push ( {
label : p ,
value : p ,
} ) ;
} ) ;
2022-08-28 17:03:15 +02:00
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 ] } ) ;
2022-09-10 09:35:52 +02:00
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 ) ;
2024-02-04 17:36:42 +01:00
if ( interaction . isMessage ) {
interaction . deleteReply ( ) ;
interaction . cleanUp ( ) ;
}
2022-08-22 20:23:27 +02:00
}
} ) ;
return ;
}
2022-06-16 09:18:39 +02:00
2023-03-20 03:57:52 +01:00
// If the video format is not one compatible with Discord, reencode it.
const bannedFormats = [ 'hevc' ] ;
const codec = await utils . getVideoCodec ( output ) ;
if ( bannedFormats . includes ( codec ) ) {
const oldOutput = output ;
output = ` ${ os . tmpdir ( ) } /264 ${ file } ` ;
2024-02-02 03:28:46 +01:00
await utils . ffmpeg ( [ '-i' , oldOutput , '-vcodec' , 'libx264' , '-acodec' , 'aac' , output ] ) ;
2023-03-20 03:57:52 +01:00
}
2023-04-11 20:31:25 +02:00
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 ) ;
}
2023-04-11 20:31:25 +02:00
2022-06-16 09:18:39 +02:00
if ( fileSize > 100 ) {
await interaction . deleteReply ( ) ;
await interaction . followUp ( 'Uh oh! The video you tried to download is too big!' , { ephemeral : true } ) ;
}
2023-04-11 20:31:25 +02:00
else if ( fileSize > maxFileSize ) {
2022-06-20 08:34:18 +02:00
const fileurl = await utils . upload ( output )
2022-06-16 09:18:39 +02:00
. catch ( err => {
console . error ( err ) ;
} ) ;
2024-01-26 18:23:09 +01:00
2023-04-11 20:31:25 +02: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 ] } ) ;
2022-06-16 09:18:39 +02:00
}
else {
await interaction . editReply ( { embeds : [ Embed ] , files : [ output ] , ephemeral : false } ) ;
}
2024-01-26 18:33:57 +01:00
2024-02-04 17:36:42 +01:00
if ( interaction . isMessage ) {
interaction . deleteReply ( ) ;
interaction . cleanUp ( ) ;
}
2022-06-16 09:18:39 +02:00
} )
. catch ( async err => {
console . error ( err ) ;
await interaction . deleteReply ( ) ;
await interaction . followUp ( { content : 'Uh oh! An error has occured!' , ephemeral : true } ) ;
} ) ;
2022-06-16 10:25:45 +02:00
return ;
2022-06-16 09:18:39 +02:00
}
2022-08-22 20:23:27 +02:00
async function compress ( input , interaction , embed ) {
const output = ` compressed ${ input } .mp4 ` ;
2023-03-20 03:57:52 +01:00
// Delete the file as it apparently don't overwrite?
2023-04-11 20:31:25 +02:00
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 ;
2023-04-11 20:31:25 +02:00
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
}