Skip to content

Instantly share code, notes, and snippets.

@weskerty
Last active April 23, 2025 23:34
Show Gist options
  • Save weskerty/ce6a4ea2c4b0a73889cae8431911734d to your computer and use it in GitHub Desktop.
Save weskerty/ce6a4ea2c4b0a73889cae8431911734d to your computer and use it in GitHub Desktop.
Download Everything, YT-DLP and CURL - DownLoadAll.
// MR. De la Comunidad para la Comunidad. Prohibida su Venta.
// El Software se proporciona bajo los términos de la Licencia MIT, excepto que usted no puede:
// 1. Vender, revender o arrendar el Software.
// 2. Cobrar a otros por el acceso, la distribución o cualquier otro uso comercial del Software.
// 3. Usar el Software como parte de un producto comercial o una oferta de servicio.
// v12 Eliminado dla curl/wget. Limite Nice. (es posible que falle en windows, requiere borrar nice)
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const { promisify } = require('util');
const { exec: execCallback } = require('child_process');
const { bot, isUrl } = require('../lib');
require('dotenv').config();
const exec = promisify(execCallback);
const FILE_TYPES = {
video: {
extensions: new Set(['mp4', 'mkv', 'avi', 'webm', 'mov', 'flv', 'm4v']),
mimetype: 'video/mp4',
},
image: {
extensions: new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg']),
mimetype: 'image/jpeg',
},
document: {
extensions: new Set(['pdf', 'epub', 'docx', 'txt', 'apk', 'apks', 'zip', 'rar', 'iso', 'ini', 'cbr', 'cbz', 'torrent', 'json', 'xml', 'html', 'css', 'js', 'csv', 'xls', 'xlsx', 'ppt', 'pptx']),
mimetypes: new Map([
['pdf', 'application/pdf'],
['epub', 'application/epub+zip'],
['docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
['txt', 'text/plain'],
['apk', 'application/vnd.android.package-archive'],
['apks', 'application/vnd.android.package-archive'],
['zip', 'application/zip'],
['rar', 'application/x-rar-compressed'],
['iso', 'application/x-iso9660-image'],
['ini', 'text/plain'],
['cbr', 'application/x-cbr'],
['cbz', 'application/x-cbz'],
['torrent', 'application/x-bittorrent'],
['json', 'application/json'],
['xml', 'application/xml'],
['html', 'text/html'],
['css', 'text/css'],
['js', 'application/javascript'],
['csv', 'text/csv'],
['xls', 'application/vnd.ms-excel'],
['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
['ppt', 'application/vnd.ms-powerpoint'],
['pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
]),
defaultMimetype: 'application/octet-stream',
},
audio: {
extensions: new Set(['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma']),
mimetype: 'audio/mpeg',
},
};
function getFileDetails(filePath) {
const ext = path.extname(filePath).slice(1).toLowerCase();
for (const [category, typeInfo] of Object.entries(FILE_TYPES)) {
if (typeInfo.extensions.has(ext)) {
return {
category,
mimetype: category === 'document'
? typeInfo.mimetypes.get(ext) || typeInfo.defaultMimetype
: typeInfo.mimetype,
};
}
}
return {
category: 'document',
mimetype: FILE_TYPES.document.defaultMimetype,
};
}
class DownloadQueue {
constructor(maxConcurrent = 2) {
this.queue = [];
this.activeDownloads = 0;
this.maxConcurrent = maxConcurrent;
}
async add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.processNext();
});
}
async processNext() {
if (this.activeDownloads >= this.maxConcurrent || this.queue.length === 0) {
return;
}
this.activeDownloads++;
const { task, resolve, reject } = this.queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.activeDownloads--;
this.processNext();
}
}
}
class MediaDownloader {
constructor() {
this.config = {
tempDir: process.env.TEMP_DOWNLOAD_DIR || path.join(process.cwd(), 'tmp'),
maxFileSize: (parseInt(process.env.MAX_UPLOAD, 10) * 1048576) || 1500000000,
ytDlpPath: path.join(process.cwd(), 'media', 'bin'),
maxConcurrent: parseInt(process.env.MAXSOLICITUD, 10) || 2,
playlistLimit: parseInt(process.env.PLAYLIST_LIMIT, 10) || 10 // limite playlist mp3
};
this.downloadQueue = new DownloadQueue(this.config.maxConcurrent);
this.ytDlpBinaries = new Map([
['win32-x64', 'yt-dlp.exe'],
['win32-ia32', 'yt-dlp_x86.exe'],
['darwin', 'yt-dlp_macos'],
['linux-x64', 'yt-dlp_linux'],
['linux-arm64', 'yt-dlp_linux_aarch64'],
['linux-arm', 'yt-dlp_linux_armv7l'],
['default', 'yt-dlp'],
]);
}
generateSafeFileName(originalName) {
const ext = path.extname(originalName);
const timestamp = Date.now();
return `download_${timestamp}${ext}`;
}
async safeExecute(command, silentError = false) {
try {
const result = await exec(command);
return result;
} catch (error) {
if (!silentError) {
console.error(`Execution failed: ${error.message}`);
}
throw new Error('Execution failed');
}
}
async isYtDlpAvailable() {
try {
await exec('yt-dlp --version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
detectYtDlpBinaryName() {
const platform = os.platform();
const arch = os.arch();
const key = `${platform}-${arch}`;
return this.ytDlpBinaries.get(key) || this.ytDlpBinaries.get('default');
}
async ensureDirectories() {
await Promise.all([
fs.mkdir(this.config.tempDir, { recursive: true }),
fs.mkdir(this.config.ytDlpPath, { recursive: true }),
]);
}
async detectYtDlpBinary(message) {
if (await this.isYtDlpAvailable()) {
return 'nice -n 7 yt-dlp';
}
const fileName = this.detectYtDlpBinaryName();
const filePath = path.join(this.config.ytDlpPath, fileName);
try {
await fs.access(filePath);
return `nice -n 7 ${filePath}`;
} catch {
return message ? await this.downloadYtDlp(message) : null;
}
}
async downloadYtDlp(message) {
await this.ensureDirectories();
const fileName = this.detectYtDlpBinaryName();
const downloadUrl = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/${fileName}`;
const filePath = path.join(this.config.ytDlpPath, fileName);
try {
await this.safeExecute(`curl -L -o "${filePath}" "${downloadUrl}"`);
if (os.platform() !== 'win32') {
await fs.chmod(filePath, '755');
}
return `nice -n 7 ${filePath}`;
} catch (error) {
// node-fetch fallback
const fetch = (await import('node-fetch')).default;
const response = await fetch(downloadUrl);
if (!response.ok) throw new Error(`Download failed: ${response.statusText}`);
const buffer = Buffer.from(await response.arrayBuffer());
await fs.writeFile(filePath, buffer);
if (os.platform() !== 'win32') {
await fs.chmod(filePath, '755');
}
return `nice -n 7 ${filePath}`;
}
}
async processDownloadedFile(message, filePath, originalFileName, outputDir) {
const { mimetype, category } = getFileDetails(filePath);
const fileBuffer = await fs.readFile(filePath);
const safeFileName = this.generateSafeFileName(originalFileName);
await message.send(
fileBuffer,
{ fileName: safeFileName, mimetype, quoted: message.data },
category
);
await fs.unlink(filePath).catch(() => {});
}
async downloadWithYtDlp(message, urls, options = '', enablePlaylist = false) {
return this.downloadQueue.add(async () => {
const ytDlpPath = await this.detectYtDlpBinary(message);
const sessionId = `yt-dlp_${Date.now()}`;
const outputDir = path.join(this.config.tempDir, sessionId);
await this.ensureDirectories();
await fs.mkdir(outputDir, { recursive: true });
for (const url of urls) {
const safePattern = path.join(outputDir, '%(title)s.%(ext)s');
let playlistFlag = enablePlaylist ? '--yes-playlist' : '--no-playlist';
let playlistItemsFlag = '';
if (enablePlaylist) {
playlistItemsFlag = `--playlist-items 1:${this.config.playlistLimit}`;
}
const baseCommand = `${ytDlpPath} --max-filesize ${this.config.maxFileSize} -o "${safePattern}" ${playlistFlag} ${playlistItemsFlag} "${url}"`;
const downloadCommand = `${baseCommand} ${options}`;
let downloadSuccess = false;
try {
await this.safeExecute(downloadCommand, true);
downloadSuccess = true;
} catch (error) {
console.error(`Failed with full options: ${error.message}`);
try {
await this.safeExecute(baseCommand, true);
downloadSuccess = true;
} catch (fallbackError) {
console.error(`Fallback also failed: ${fallbackError.message}`);
try {
await this.updateYtDlp(message, true);
} catch (updateError) {
console.error(`Update failed: ${updateError.message}`);
}
}
}
if (downloadSuccess) {
try {
const files = await fs.readdir(outputDir);
for (const file of files) {
try {
await this.processDownloadedFile(
message,
path.join(outputDir, file),
file,
outputDir
);
} catch (processError) {
console.error(`Failed to process file ${file}: ${processError.message}`);
}
}
} catch (error) {
console.error(`Failed to read output directory: ${error.message}`);
}
}
}
await fs.rm(outputDir, { recursive: true, force: true }).catch(() => {});
});
}
async updateYtDlp(message, silent = false) {
try {
if (await this.isYtDlpAvailable()) {
await this.safeExecute('pip install -U --pre "yt-dlp[default]" ');
if (!silent) {
await message.send('✅ yt-dlp Updated pip', { quoted: message.data });
}
} else {
const filePath = await this.downloadYtDlp(message);
if (!silent) {
await message.send(`✅ yt-dlp Updated Bin _${filePath}_`, { quoted: message.data });
}
}
} catch (error) {
if (!silent) {
await message.send(`✅ yt-dlp Update attempted`, { quoted: message.data });
}
}
}
async searchAndDownload(message, searchQuery, isVideo = false) {
return this.downloadQueue.add(async () => {
const sessionId = `yt-dlp_${Date.now()}`;
const outputDir = path.join(this.config.tempDir, sessionId);
await this.ensureDirectories();
await fs.mkdir(outputDir, { recursive: true });
const safePattern = path.join(outputDir, 'download.%(ext)s');
const ytDlpPath = await this.detectYtDlpBinary(message);
const searchSources = [
{ source: 'ytsearch', name: 'YouTube' },
...(isVideo ? [] : [
{ source: 'scsearch', name: 'SoundCloud' },
{ source: 'nicosearch', name: 'NicoNico' }
])
];
let success = false;
for (const { source, name } of searchSources) {
if (success) break;
try {
const baseCommand = `${ytDlpPath} --max-filesize ${this.config.maxFileSize} --playlist-items 1 -o "${safePattern}" --ignore-errors --no-abort-on-error`;
const videoOptions = isVideo ? '-f "bestvideo[height<=720][ext=mp4][vcodec=h264]+bestaudio[acodec=aac]/best[height<=720][vcodec=h264]/best[ext=mp4]/best" --merge-output-format mp4 ' : '-x --audio-format mp3';
const command = `${baseCommand} ${videoOptions} "${source}10:${searchQuery}"`;
try {
await this.safeExecute(command, true);
} catch (error) {
await this.safeExecute(`${baseCommand} "${source}10:${searchQuery}"`, true);
}
const files = await fs.readdir(outputDir);
if (files.length > 0) {
await Promise.all(
files.map(file => this.processDownloadedFile(
message,
path.join(outputDir, file),
file,
outputDir
))
);
success = true;
break;
}
} catch (error) {
await fs.rm(outputDir, { recursive: true, force: true }).catch(() => {});
await fs.mkdir(outputDir, { recursive: true });
}
}
if (!success) {
console.error(`No results found for ${searchQuery}`);
}
await fs.rm(outputDir, { recursive: true, force: true }).catch(() => {});
});
}
}
const mediaDownloader = new MediaDownloader();
bot(
{
pattern: 'dla ?(.*)',
fromMe: true,
desc: 'Download All Media Web Site.',
type: 'download',
},
async (message, match) => {
const input = match.trim() || message.reply_message?.text || '';
if (!input) {
await message.send(
'> 🎶Search and Download Song:\n`dla` <query>\n' +
'> 🎥Search and Download Video:\n`dla vd` <query>\n' +
'> ⬇️Download All Media: \n`dla` <url> _YT-DLP FLAGS_ \n' +
'> 🎵Download All Audio from Playlist: \n`dla mp3` <url> \n' +
'> 🆙Fail Download? Update YT-DLP: \n`dla update` \n' +
'> 🌐More Info:\ngithub.com/yt-dlp/yt-dlp/blob/master/README.md#usage-and-options',
{ quoted: message.data }
);
return;
}
try {
const args = input.match(/[^\s"]+|"([^"]*)"/g)?.map(arg =>
arg.startsWith('"') && arg.endsWith('"') ? arg.slice(1, -1) : arg
) || [];
const command = args[0];
const remainingArgs = args.slice(1);
const urls = remainingArgs.filter(arg => isUrl(arg));
if (!urls.length && command !== 'update') {
if (input.includes('://')) {
const inputUrl = input.match(/(https?:\/\/[^\s]+)/)?.[1];
if (inputUrl) {
const options = input.replace(inputUrl, '').trim();
await mediaDownloader.downloadWithYtDlp(message, [inputUrl], `-f "bestvideo[height<=720][ext=mp4][vcodec=h264]+bestaudio[acodec=aac]/best[height<=720][vcodec=h264]/best[ext=mp4]/best" --merge-output-format mp4 ${options}`);
return;
}
}
if (command === 'vd') {
await mediaDownloader.searchAndDownload(message, remainingArgs.join(' '), true);
} else {
await mediaDownloader.searchAndDownload(message, input, false);
}
return;
}
switch (command) {
case 'update':
await mediaDownloader.updateYtDlp(message);
break;
case 'mp3':
if (urls.length) {
const options = remainingArgs
.filter(arg => !isUrl(arg))
.join(' ');
await mediaDownloader.downloadWithYtDlp(
message,
urls,
`-x --audio-format mp3 ${options}`,
true
);
}
break;
default:
const options = remainingArgs
.filter(arg => !isUrl(arg))
.join(' ');
await mediaDownloader.downloadWithYtDlp(
message,
urls,
`-f "bestvideo[height<=720][ext=mp4][vcodec=h264]+bestaudio[acodec=aac]/best[height<=720][vcodec=h264]/best[ext=mp4]/best" --merge-output-format mp4 ${options}`
);
break;
}
} catch (error) {
console.error(`Error in dla command: ${error.message}`);
}
}
);
module.exports = { mediaDownloader };
@Jbcreates
Copy link

I can't download YouTube shorts

@weskerty
Copy link
Author

I can't download YouTube shorts

What error does it show?
Because it works perfectly for me.
image

@Jbcreates
Copy link

Jbcreates commented Dec 31, 2024 via email

@weskerty
Copy link
Author

It showing can't read properties of mp4

Does this happen with all shorts?
What are you running it on? What server? What Linux version?

@Ayomide661
Copy link

I can't download YouTube shorts

What error does it show? Because it works perfectly for me. image

Well, that's because you are using termux right?

@weskerty
Copy link
Author

Well, that's because you are using termux right?

Yes, I use Termux.
If you use an external server that doesn't support ffmpeg, there's nothing you can do, this is not a problem with the bot, the script or yt-dlp, it's a problem with your server.

@weskerty
Copy link
Author

weskerty commented Feb 6, 2025

image

@Ayomide661
Copy link

I created a custom plugin that can download node modules directly to the node modules folder
Since some panel don't have interactive shell for users to install a missing module when trying to try out a new plugin.

@Ayomide661
Copy link

It's a private gist but I can share the url if anyone desire

@weskerty
Copy link
Author

weskerty commented Mar 9, 2025

I created a custom plugin that can download node modules directly to the node modules folder Since some panel don't have interactive shell for users to install a missing module when trying to try out a new plugin.

What plugin did you make that requires other node modules?
There is already an alternative to this.
The Linux plugin or CMD (my gist) where you just have to do
.cmd npm install <module> --force --no-save and that's it.

I saw your Gist. Are you talking about a bot external to Levanter?

@Ayomide661
Copy link

here is the plugin, when you asked what type of plugin I made that would require one to need to install a custom module.
There's was a plugin I was making over 2 months ago before dropping it because of a reason.

So back to the reason, my plugin(an interactive multi users RPG game with economy where every players that will be connected and be able to interact with each other) required mongoose module which is not pre-installed.

I created a custom plugin that can download node modules directly to the node modules folder Since some panel don't have interactive shell for users to install a missing module when trying to try out a new plugin.

What plugin did you make that requires other node modules? There is already an alternative to this. The Linux plugin or CMD (my gist) where you just have to do .cmd npm install <module> --force --no-save and that's it.

I saw your Gist. Are you talking about a bot external to Levanter?

@Ayomide661
Copy link

@weskerty
So i made the plugin.
And tbh, I didn't know that this existed before I created mine fr 🙂.

@Ayomide661
Copy link

And I have a question
@weskerty
Is it possible to achieve the goal of making multiple users have a unique account for the game when invoking a command from the bot.

My reason for asking this is that levanter is a one user per bot policy and every available commands are for the users only, but commands can be made available with the zushi command.

But even when using zushi, the bot will react as if you are the one invoking a command in a group when others use the set commands.

Does WhatsApp have a unique way to recognize different users in a group so that the bot will be able to know that Participant A is different from Participant B in a group

@weskerty
Copy link
Author

weskerty commented Apr 3, 2025

Does WhatsApp have a unique way to recognize different users in a group so that the bot will be able to know that Participant A is different from Participant B in a group

Yes. The Jids.
But I haven't seen how it works in Levante yet.
Do you want everyone to log in to YT or other services?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment