-
-
Save kajju027/168c1db126f207d9015d50b3e996724c to your computer and use it in GitHub Desktop.
Download Everything, YT-DLP and CURL - DownLoadAll.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // dla.js is a complement to LevanterBot: https://github.com/lyfe00011/levanter | |
| // Copyright (C) 2025 Weskerty | |
| // | |
| // Este programa se distribuye bajo los términos de la Licencia Pública General Affero de GNU (AGPLv3). | |
| // Usted puede usarlo, modificarlo y redistribuirlo bajo esa licencia. | |
| // Este software se proporciona SIN GARANTÍA alguna. | |
| // Licencia completa: https://www.gnu.org/licenses/agpl-3.0.html | |
| const fs = require('fs').promises; | |
| const path = require('path'); | |
| const os = require('os'); | |
| const { promisify } = require('util'); | |
| const { execFile } = require('child_process'); | |
| const { bot, isUrl } = require('../lib'); | |
| const execFileAsync = promisify(execFile); | |
| 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', | |
| }, | |
| }; | |
| const TIMEOUT_MS = 300000; | |
| function withTimeout(promise, ms = TIMEOUT_MS, errorMsg = 'Operation timeout') { | |
| return Promise.race([ | |
| promise, | |
| new Promise((_, reject) => | |
| setTimeout(() => reject(new Error(errorMsg)), ms) | |
| ) | |
| ]); | |
| } | |
| 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; | |
| this.isProcessing = false; | |
| } | |
| async add(task) { | |
| return new Promise((resolve, reject) => { | |
| this.queue.push({ task, resolve, reject }); | |
| this.processNext(); | |
| }); | |
| } | |
| async processNext() { | |
| if (this.isProcessing) { | |
| return; | |
| } | |
| this.isProcessing = true; | |
| try { | |
| while (this.activeDownloads < this.maxConcurrent && this.queue.length > 0) { | |
| const { task, resolve, reject } = this.queue.shift(); | |
| this.activeDownloads++; | |
| task() | |
| .then(resolve) | |
| .catch(reject) | |
| .finally(() => { | |
| this.activeDownloads--; | |
| setImmediate(() => this.processNext()); | |
| }); | |
| } | |
| } finally { | |
| this.isProcessing = false; | |
| } | |
| } | |
| } | |
| class MediaDownloader { | |
| constructor() { | |
| this.ctx = null; | |
| this.ytDlpBinaryPath = null; | |
| 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'], | |
| ]); | |
| this.presetFormats = { | |
| video: ['-f', 'sd/18/bestvideo[height<=720][vcodec*=h264]+bestaudio[acodec*=aac]/bestvideo[height<=720][vcodec*=h264]+bestaudio[acodec*=mp4a]/bestvideo[height<=720][vcodec*=h264]+bestaudio/bestvideo[height<=720]+bestaudio/bestvideo[vcodec*=h264]+bestaudio[acodec*=aac]/bestvideo[vcodec*=h264]+bestaudio[acodec*=mp4a]/bestvideo[vcodec*=h264]+bestaudio/bestvideo+bestaudio/best', '--sponsorblock-remove', 'all', '--embed-chapters', '--embed-metadata'], | |
| audio: ['-f', 'ba/best', '-x', '--audio-format', 'mp3', '--audio-quality', '0', '--sponsorblock-remove', 'all'], | |
| playlist: ['--yes-playlist'], | |
| noPlaylist: ['--no-playlist'] | |
| }; | |
| this.commonArgs = [ | |
| '--restrict-filenames', | |
| '--extractor-retries', '3', | |
| '--fragment-retries', '3', | |
| '--compat-options', 'no-youtube-unavailable-videos', | |
| '--ignore-errors', | |
| '--no-abort-on-error', | |
| //'--extractor-args', 'youtube:player_client=android,ios,web', | |
| '--js-runtimes', 'node' | |
| ]; | |
| } | |
| setContext(ctx) { | |
| this.ctx = ctx; | |
| this.config = { | |
| tempDir: ctx.TEMP_DOWNLOAD_DIR || path.join(process.cwd(), 'tmp'), | |
| maxFileSize: (parseInt(ctx.MAX_UPLOAD, 10) * 1048576) || 1500000000, | |
| ytDlpPath: path.join(process.cwd(), 'media', 'bin'), | |
| maxConcurrent: parseInt(ctx.MAXSOLICITUD, 10) || 2, | |
| playlistLimit: parseInt(ctx.PLAYLIST_LIMIT, 10) || 20, | |
| cookies: ctx.COOKIES || null, | |
| enableMp3Zip: ctx.DLAMP3ZIP === undefined || ctx.DLAMP3ZIP === 'TRUE' || ctx.DLAMP3ZIP === 'true' | |
| }; | |
| this.downloadQueue = new DownloadQueue(this.config.maxConcurrent); | |
| } | |
| normalizeTimeSegment(segment) { | |
| const parts = segment.split(':'); | |
| if (parts.length === 2) { | |
| const [min, sec] = parts; | |
| return `00:${min.padStart(2, '0')}:${sec.padStart(2, '0')}`; | |
| } else if (parts.length === 3) { | |
| const [h, m, s] = parts; | |
| return `${h.padStart(2, '0')}:${m.padStart(2, '0')}:${s.padStart(2, '0')}`; | |
| } | |
| return segment; | |
| } | |
| parseTimeRanges(timeString) { | |
| if (!timeString || !timeString.trim()) { | |
| return null; | |
| } | |
| const ranges = timeString.split(/\s+|,/).filter(r => r.trim()); | |
| const normalizedRanges = []; | |
| for (const range of ranges) { | |
| if (range.includes('-')) { | |
| const [start, end] = range.split('-'); | |
| const normalizedStart = this.normalizeTimeSegment(start.trim()); | |
| const normalizedEnd = this.normalizeTimeSegment(end.trim()); | |
| normalizedRanges.push(`*${normalizedStart}-${normalizedEnd}`); | |
| } | |
| } | |
| return normalizedRanges.length > 0 ? normalizedRanges.join(',') : null; | |
| } | |
| buildCookiesArgs() { | |
| const cookiesPath = path.join(this.config.ytDlpPath, 'yt-dlp.cookies.txt'); | |
| try { | |
| require('fs').accessSync(cookiesPath, require('fs').constants.F_OK); | |
| return ['--cookies', cookiesPath]; | |
| } catch { | |
| return this.config.cookies ? ['--cookies', this.config.cookies] : []; | |
| } | |
| } | |
| async checkZipAvailable() { | |
| try { | |
| await withTimeout(execFileAsync('which', ['zip']), TIMEOUT_MS, 'Timeout checking zip availability'); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| async compressWithZip(outputDir) { | |
| const outputZip = path.join(this.config.tempDir, `playlist_${Date.now()}.zip`); | |
| const files = await withTimeout(fs.readdir(outputDir), TIMEOUT_MS, 'Timeout reading directory for zip'); | |
| await withTimeout( | |
| execFileAsync('zip', ['-r', outputZip, ...files], { | |
| cwd: outputDir, | |
| maxBuffer: 1024 * 1024 * 100 | |
| }), | |
| TIMEOUT_MS, | |
| 'Timeout creating zip file' | |
| ); | |
| return outputZip; | |
| } | |
| async safeExecuteYtDlp(args) { | |
| return await execFileAsync(this.ytDlpBinaryPath, args, { | |
| maxBuffer: 1024 * 1024 * 100, | |
| timeout: TIMEOUT_MS | |
| }); | |
| } | |
| async isYtDlpAvailable() { | |
| try { | |
| await withTimeout(execFileAsync('yt-dlp', ['--version']), TIMEOUT_MS, 'Timeout checking yt-dlp'); | |
| 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([ | |
| withTimeout(fs.mkdir(this.config.tempDir, { recursive: true }), TIMEOUT_MS, 'Timeout creating temp directory'), | |
| withTimeout(fs.mkdir(this.config.ytDlpPath, { recursive: true }), TIMEOUT_MS, 'Timeout creating yt-dlp directory'), | |
| ]); | |
| } | |
| async detectYtDlpBinary(message) { | |
| if (this.ytDlpBinaryPath) { | |
| return this.ytDlpBinaryPath; | |
| } | |
| if (await this.isYtDlpAvailable()) { | |
| this.ytDlpBinaryPath = 'yt-dlp'; | |
| return this.ytDlpBinaryPath; | |
| } | |
| const fileName = this.detectYtDlpBinaryName(); | |
| const filePath = path.join(this.config.ytDlpPath, fileName); | |
| try { | |
| await withTimeout(fs.access(filePath), TIMEOUT_MS, 'Timeout accessing yt-dlp binary'); | |
| this.ytDlpBinaryPath = filePath; | |
| return this.ytDlpBinaryPath; | |
| } catch { | |
| if (message) { | |
| this.ytDlpBinaryPath = await this.downloadYtDlp(message); | |
| return this.ytDlpBinaryPath; | |
| } | |
| return 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); | |
| const fetch = (await import('node-fetch')).default; | |
| const response = await withTimeout(fetch(downloadUrl), TIMEOUT_MS, 'Timeout downloading yt-dlp'); | |
| if (!response.ok) { | |
| throw new Error(`Download failed: ${response.statusText}`); | |
| } | |
| const buffer = Buffer.from(await withTimeout(response.arrayBuffer(), TIMEOUT_MS, 'Timeout reading download response')); | |
| await withTimeout(fs.writeFile(filePath, buffer), TIMEOUT_MS, 'Timeout writing yt-dlp binary'); | |
| if (os.platform() !== 'win32') { | |
| await withTimeout(fs.chmod(filePath, '755'), TIMEOUT_MS, 'Timeout setting permissions'); | |
| } | |
| return filePath; | |
| } | |
| async loadInfoJson(outputDir) { | |
| // Modified: also capture thumbnail for each info.json and generic if available | |
| const files = await withTimeout(fs.readdir(outputDir), TIMEOUT_MS, 'Timeout reading directory for info.json'); | |
| const jsonFiles = files.filter(f => f.endsWith('.info.json')); | |
| const infoMap = new Map(); | |
| let genericInfo = null; | |
| for (const jsonFile of jsonFiles) { | |
| try { | |
| const jsonPath = path.join(outputDir, jsonFile); | |
| const content = await withTimeout(fs.readFile(jsonPath, 'utf8'), TIMEOUT_MS, 'Timeout reading info.json'); | |
| const data = JSON.parse(content); | |
| const baseName = jsonFile.replace('.info.json', ''); | |
| const mediaFiles = files.filter(f => { | |
| const fileBase = f.substring(0, f.lastIndexOf('.')); | |
| return fileBase === baseName && !f.endsWith('.info.json'); | |
| }); | |
| const infoRecord = { | |
| title: data.title || '', | |
| description: data.description || '', | |
| uploader: data.uploader || data.uploader_id || '', | |
| thumbnail: data.thumbnail || null, | |
| webpage_url: data.webpage_url || data.id || null | |
| }; | |
| if (mediaFiles.length > 0) { | |
| for (const mediaFile of mediaFiles) { | |
| infoMap.set(mediaFile, infoRecord); | |
| } | |
| } else { | |
| if (!genericInfo) { | |
| genericInfo = infoRecord; | |
| } | |
| } | |
| } catch (error) { | |
| // ignore malformed json files | |
| continue; | |
| } | |
| } | |
| return { infoMap, genericInfo }; | |
| } | |
| formatCaption(info) { | |
| if (!info) return null; | |
| // Build caption with title + uploader + description (short) | |
| const title = info.title || ''; | |
| const uploader = info.uploader ? ` - ${info.uploader}` : ''; | |
| const desc = info.description ? `\n\n${info.description}` : ''; | |
| const titleLine = title ? `> ${title}${uploader}` : (info.uploader ? `> ${info.uploader}` : ''); | |
| const caption = desc ? `${titleLine}${desc}` : titleLine; | |
| return caption || null; | |
| } | |
| async processDownloadedFile(message, filePath, originalFileName, quotedMessage, caption = null) { | |
| const { mimetype, category } = getFileDetails(filePath); | |
| const fileBuffer = await withTimeout(fs.readFile(filePath), TIMEOUT_MS, 'Timeout reading file for upload'); | |
| const options = { fileName: originalFileName, mimetype, quoted: quotedMessage }; | |
| if (caption) { | |
| options.caption = caption; | |
| } | |
| await message.send(fileBuffer, options, category); | |
| await this.safeCleanup(filePath); | |
| } | |
| async safeCleanup(target, retries = 3) { | |
| for (let i = 0; i < retries; i++) { | |
| try { | |
| const stats = await fs.stat(target); | |
| if (stats.isDirectory()) { | |
| await withTimeout(fs.rm(target, { recursive: true, force: true }), TIMEOUT_MS, 'Timeout cleaning directory'); | |
| } else { | |
| await withTimeout(fs.unlink(target), TIMEOUT_MS, 'Timeout cleaning file'); | |
| } | |
| return; | |
| } catch (err) { | |
| if (err.code === 'ENOENT') { | |
| return; | |
| } | |
| if (i === retries - 1) { | |
| console.error(`Failed to cleanup ${target} after ${retries} attempts:`, err.message); | |
| return; | |
| } | |
| await new Promise(r => setTimeout(r, 1000 * (i + 1))); | |
| } | |
| } | |
| } | |
| async updateYtDlp(message) { | |
| const ytDlpPath = await this.detectYtDlpBinary(message); | |
| try { | |
| const result = await this.safeExecuteYtDlp(['--update-to', 'master']); | |
| const updateOutput = result.stdout || result.stderr || 'yt-dlp actualizado exitosamente'; | |
| return `> 🔄UpdateBin \n${updateOutput}`; | |
| } catch (updateError) { | |
| return null; | |
| } | |
| } | |
| async uploadCookies(message, cookieText = null) { | |
| const quotedMessage = message.reply_message; | |
| let cookieContent = null; | |
| if (cookieText) { | |
| cookieContent = cookieText; | |
| } else if (quotedMessage) { | |
| const mediaBuffer = await quotedMessage.downloadMediaMessage(); | |
| if (!mediaBuffer) { | |
| throw new Error('Error al descargar el archivo de cookies'); | |
| } | |
| cookieContent = mediaBuffer.toString(); | |
| } else { | |
| throw new Error('Las cookies deben ser texto o citar un archivo de cookies'); | |
| } | |
| await this.ensureDirectories(); | |
| const cookiesPath = path.join(this.config.ytDlpPath, 'yt-dlp.cookies.txt'); | |
| await withTimeout(fs.writeFile(cookiesPath, cookieContent), TIMEOUT_MS, 'Timeout writing cookies file'); | |
| await message.send('Cookies subida', { quoted: message.quoted }); | |
| } | |
| async downloadWithYtDlp(message, urls, formatType = 'video', enablePlaylist = false, timeRanges = null) { | |
| 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); | |
| const cookiesArgs = this.buildCookiesArgs(); | |
| await this.ensureDirectories(); | |
| await withTimeout(fs.mkdir(outputDir, { recursive: true }), TIMEOUT_MS, 'Timeout creating session directory'); | |
| const isMp3Playlist = formatType === 'audio' && enablePlaylist; | |
| const isDirectLink = formatType === 'video' && !enablePlaylist; | |
| let lastError = null; | |
| try { | |
| for (const url of urls) { | |
| const outputTemplate = path.join(outputDir, '%(title).70s.%(ext)s'); | |
| const playlistArgs = enablePlaylist ? this.presetFormats.playlist : this.presetFormats.noPlaylist; | |
| const playlistItemsArgs = enablePlaylist ? ['--playlist-items', `1:${this.config.playlistLimit}`] : []; | |
| const timeRangeArgs = timeRanges && !enablePlaylist ? ['--download-sections', timeRanges] : []; | |
| const infoJsonArgs = isDirectLink ? ['--write-info-json'] : []; | |
| const args = [ | |
| '--max-filesize', this.config.maxFileSize.toString(), | |
| ...this.commonArgs, | |
| ...playlistArgs, | |
| ...playlistItemsArgs, | |
| ...cookiesArgs, | |
| ...timeRangeArgs, | |
| ...infoJsonArgs, | |
| ...this.presetFormats[formatType], | |
| '-o', outputTemplate, | |
| url | |
| ]; | |
| try { | |
| await this.safeExecuteYtDlp(args); | |
| } catch (error) { | |
| lastError = error; | |
| } | |
| } | |
| const allFiles = await withTimeout(fs.readdir(outputDir), TIMEOUT_MS, 'Timeout reading output directory'); | |
| const files = allFiles.filter(f => !f.endsWith('.info.json')); | |
| if (files.length === 0) { | |
| await this.safeCleanup(outputDir); | |
| if (lastError) { | |
| const errorMessage = lastError.stderr || lastError.message || 'Error desconocido'; | |
| const updateMessage = await this.updateYtDlp(message); | |
| const fullError = updateMessage | |
| ? `${errorMessage}\n\n${updateMessage}` | |
| : errorMessage; | |
| throw new Error(fullError); | |
| } | |
| throw new Error('No se descargaron archivos'); | |
| } | |
| let infoMap = new Map(); | |
| let genericInfo = null; | |
| if (isDirectLink) { | |
| const infoData = await this.loadInfoJson(outputDir); | |
| infoMap = infoData.infoMap; | |
| genericInfo = infoData.genericInfo; | |
| } | |
| if (isMp3Playlist && this.config.enableMp3Zip && files.length > 1) { | |
| const zipAvailable = await this.checkZipAvailable(); | |
| if (zipAvailable) { | |
| try { | |
| const zipPath = await this.compressWithZip(outputDir); | |
| const { mimetype } = getFileDetails(zipPath); | |
| await message.send( | |
| await withTimeout(fs.readFile(zipPath), TIMEOUT_MS, 'Timeout reading zip file'), | |
| { | |
| fileName: path.basename(zipPath), | |
| mimetype, | |
| caption: 'Usa 7zip para descomprimir', | |
| quoted: message.quoted | |
| }, | |
| 'document' | |
| ); | |
| await this.safeCleanup(zipPath); | |
| } catch (zipError) { | |
| for (const file of files) { | |
| await this.processDownloadedFile( | |
| message, | |
| path.join(outputDir, file), | |
| file, | |
| message.quoted | |
| ); | |
| } | |
| } | |
| } else { | |
| for (const file of files) { | |
| await this.processDownloadedFile( | |
| message, | |
| path.join(outputDir, file), | |
| file, | |
| message.quoted | |
| ); | |
| } | |
| } | |
| } else { | |
| let firstFileSent = false; | |
| for (const file of files) { | |
| let caption = null; | |
| if (isDirectLink) { | |
| if (infoMap.has(file)) { | |
| caption = this.formatCaption(infoMap.get(file)); | |
| } else if (!firstFileSent && genericInfo) { | |
| caption = this.formatCaption(genericInfo); | |
| } | |
| } | |
| await this.processDownloadedFile( | |
| message, | |
| path.join(outputDir, file), | |
| file, | |
| message.quoted, | |
| caption | |
| ); | |
| firstFileSent = true; | |
| } | |
| } | |
| if (lastError) { | |
| const errorMessage = lastError.stderr || lastError.message || 'Error desconocido'; | |
| const updateMessage = await this.updateYtDlp(message); | |
| const fullError = updateMessage | |
| ? `${errorMessage}\n\n${updateMessage}` | |
| : errorMessage; | |
| throw new Error(fullError); | |
| } | |
| } finally { | |
| await this.safeCleanup(outputDir); | |
| } | |
| }); | |
| } | |
| 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); | |
| const cookiesArgs = this.buildCookiesArgs(); | |
| await this.ensureDirectories(); | |
| await withTimeout(fs.mkdir(outputDir, { recursive: true }), TIMEOUT_MS, 'Timeout creating session directory'); | |
| const outputTemplate = path.join(outputDir, '%(title).70s.%(ext)s'); | |
| const ytDlpPath = await this.detectYtDlpBinary(message); | |
| const formatArgs = isVideo ? this.presetFormats.video : this.presetFormats.audio; | |
| const searchSources = [ | |
| { prefix: '', useDefaultSearch: true }, | |
| ...(isVideo ? [ | |
| { prefix: 'ytsearch10:' }, | |
| { prefix: 'gvsearch10:' }, | |
| { prefix: 'yvsearch10:' }, | |
| { prefix: 'dailymotionsearch:' } | |
| ] : [ | |
| { prefix: 'ytsearch10:' }, | |
| { prefix: 'scsearch10:' }, | |
| { prefix: 'mailrusearch10:' }, | |
| { prefix: 'nicosearch10:' } | |
| ]) | |
| ]; | |
| let lastError = null; | |
| try { | |
| for (const { prefix, useDefaultSearch } of searchSources) { | |
| const searchUrl = useDefaultSearch ? searchQuery : `${prefix}${searchQuery}`; | |
| const searchArgs = useDefaultSearch ? ['--default-search', 'auto'] : []; | |
| const args = [ | |
| ...searchArgs, | |
| '--max-filesize', this.config.maxFileSize.toString(), | |
| ...this.commonArgs, | |
| '--playlist-items', '1', | |
| ...formatArgs, | |
| ...cookiesArgs, | |
| '-o', outputTemplate, | |
| searchUrl | |
| ]; | |
| try { | |
| await this.safeExecuteYtDlp(args); | |
| const files = await withTimeout(fs.readdir(outputDir), TIMEOUT_MS, 'Timeout reading output directory'); | |
| if (files.length > 0) { | |
| // NEW: load info.json to obtain title/thumbnail/description && send thumbnail + caption + file | |
| const infoData = await this.loadInfoJson(outputDir); | |
| const infoMap = infoData.infoMap; | |
| const genericInfo = infoData.genericInfo; | |
| // We will send each found file: first thumbnail (if exists), then the file with caption | |
| await Promise.all( | |
| files.map(async file => { | |
| const filePath = path.join(outputDir, file); | |
| let caption = null; | |
| let thumbUrl = null; | |
| if (infoMap.has(file)) { | |
| const info = infoMap.get(file); | |
| caption = this.formatCaption(info); | |
| thumbUrl = info.thumbnail || null; | |
| } else if (genericInfo) { | |
| caption = this.formatCaption(genericInfo); | |
| thumbUrl = genericInfo.thumbnail || null; | |
| } | |
| // If thumbnail URL exists, try to send it first as an image with caption. | |
| // Some messaging libs accept { url: thumbUrl } as media source. | |
| if (thumbUrl) { | |
| try { | |
| // Provide a lightweight image send with caption (caption is optional here) | |
| await message.send( | |
| { url: thumbUrl }, | |
| { | |
| fileName: 'thumbnail.jpg', | |
| mimetype: 'image/jpeg', | |
| caption: caption || undefined, | |
| quoted: message.quoted | |
| }, | |
| 'image' | |
| ); | |
| // After sending thumbnail with caption, we may want to avoid duplicating the caption | |
| // for the audio file. But many users prefer the caption with the audio file as well. | |
| // We'll include caption again with the audio file for compatibility with clients that | |
| // don't show image captions prominently. | |
| } catch (err) { | |
| // ignore thumbnail send errors and continue to send audio | |
| } | |
| } | |
| // Send the actual file (audio/video) with caption | |
| await this.processDownloadedFile( | |
| message, | |
| filePath, | |
| file, | |
| message.quoted, | |
| caption | |
| ); | |
| }) | |
| ); | |
| await this.safeCleanup(outputDir); | |
| return; | |
| } | |
| } catch (error) { | |
| lastError = error; | |
| // If partial files exist, try to send them | |
| const currentFiles = await withTimeout(fs.readdir(outputDir), TIMEOUT_MS, 'Timeout checking files'); | |
| if (currentFiles.length > 0) { | |
| const infoData = await this.loadInfoJson(outputDir); | |
| const infoMap = infoData.infoMap; | |
| const genericInfo = infoData.genericInfo; | |
| await Promise.all( | |
| currentFiles.map(async file => { | |
| const filePath = path.join(outputDir, file); | |
| let caption = null; | |
| let thumbUrl = null; | |
| if (infoMap.has(file)) { | |
| const info = infoMap.get(file); | |
| caption = this.formatCaption(info); | |
| thumbUrl = info.thumbnail || null; | |
| } else if (genericInfo) { | |
| caption = this.formatCaption(genericInfo); | |
| thumbUrl = genericInfo.thumbnail || null; | |
| } | |
| if (thumbUrl) { | |
| try { | |
| await message.send( | |
| { url: thumbUrl }, | |
| { | |
| fileName: 'thumbnail.jpg', | |
| mimetype: 'image/jpeg', | |
| caption: caption || undefined, | |
| quoted: message.quoted | |
| }, | |
| 'image' | |
| ); | |
| } catch (err) {} | |
| } | |
| await this.processDownloadedFile( | |
| message, | |
| filePath, | |
| file, | |
| message.quoted, | |
| caption | |
| ); | |
| }) | |
| ); | |
| await this.safeCleanup(outputDir); | |
| return; | |
| } | |
| } | |
| } | |
| await this.safeCleanup(outputDir); | |
| const errorMessage = lastError?.stderr || lastError?.message || 'Error desconocido'; | |
| const updateMessage = await this.updateYtDlp(message); | |
| const fullError = updateMessage ? `${errorMessage}\n\n${updateMessage}` : errorMessage; | |
| throw new Error(fullError || `No se pudo descargar: ${searchQuery}`); | |
| } catch (error) { | |
| await this.safeCleanup(outputDir); | |
| throw error; | |
| } | |
| }); | |
| } | |
| } | |
| const mediaDownloader = new MediaDownloader(); | |
| bot( | |
| { | |
| pattern: 'dla ?(.*)', | |
| fromMe: true, | |
| desc: 'Download All Media Web Site.', | |
| type: 'download', | |
| }, | |
| async (message, match, ctx) => { | |
| mediaDownloader.setContext(ctx); | |
| 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 Media: \n`dla` <url>\n' + | |
| '> ✂️Download Segment: \n`dla` <url> `--t` <time>\n' + | |
| '> 🎵Download Audio from Playlist: \n`dla mp3` <url>\n' + | |
| '> 🍪Cookies: `dla cookies` <cookie_text>\n' + | |
| '> 📖Help: github.com/yt-dlp/yt-dlp', | |
| { quoted: message.quoted } | |
| ); | |
| return; | |
| } | |
| if (input.toLowerCase().startsWith('cookies')) { | |
| const cookieText = input.substring('cookies'.length).trim(); | |
| await mediaDownloader.uploadCookies(message, cookieText || null); | |
| return; | |
| } | |
| const urlRegex = /(https?:\/\/[^\s]+)/g; | |
| const urls = (input.match(urlRegex) || []).filter(url => isUrl(url)); | |
| if (urls.length > 0) { | |
| let commandPart = input; | |
| urls.forEach(url => { | |
| commandPart = commandPart.replace(url, '').trim(); | |
| }); | |
| const parts = commandPart.split(/\s+/).filter(p => p); | |
| const firstPart = parts[0] || ''; | |
| if (firstPart === 'mp3') { | |
| await mediaDownloader.downloadWithYtDlp(message, urls, 'audio', true, null); | |
| } else { | |
| let timeRanges = null; | |
| const timeIndex = parts.indexOf('--t'); | |
| if (timeIndex !== -1 && parts[timeIndex + 1]) { | |
| const timeString = parts.slice(timeIndex + 1).join(' '); | |
| timeRanges = mediaDownloader.parseTimeRanges(timeString); | |
| } | |
| await mediaDownloader.downloadWithYtDlp(message, urls, 'video', false, timeRanges); | |
| } | |
| return; | |
| } | |
| const args = input.trim().split(/\s+/); | |
| const command = args[0]; | |
| const remainingArgs = args.slice(1); | |
| if (command === 'vd') { | |
| await mediaDownloader.searchAndDownload(message, remainingArgs.join(' '), true); | |
| } else { | |
| await mediaDownloader.searchAndDownload(message, input, false); | |
| } | |
| } | |
| ); | |
| module.exports = { mediaDownloader }; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment