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 };
@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