Skip to content

Instantly share code, notes, and snippets.

@weskerty
Last active August 26, 2025 13:27
Show Gist options
  • Select an option

  • Save weskerty/2711c05a05e1dd41dcc6a00f078595cc to your computer and use it in GitHub Desktop.

Select an option

Save weskerty/2711c05a05e1dd41dcc6a00f078595cc to your computer and use it in GitHub Desktop.
Descarga Peliculas en Español
const axios = require('axios');
const cheerio = require('cheerio');
const { chromium } = require('playwright');
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 } = require('../lib');
require('dotenv').config();
const exec = promisify(execCallback);
const searchResults = new Map();
const FILE_TYPES = {
video: {
extensions: new Set(['mp4', 'mkv', 'avi', 'webm', 'mov', 'flv', 'm4v']),
mimetype: 'video/mp4',
}
};
function getFileDetails(filePath) {
const ext = path.extname(filePath).slice(1).toLowerCase();
if (FILE_TYPES.video.extensions.has(ext)) {
return {
category: 'video',
mimetype: FILE_TYPES.video.mimetype,
};
}
return {
category: 'video',
mimetype: FILE_TYPES.video.mimetype,
};
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class CuevanaDownloader {
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'),
cookies: process.env.COOKIES || null
};
this.ytDlpCommand = 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.commonFlags = [
'--restrict-filenames',
'--extractor-retries 3',
'--fragment-retries 3',
'--ignore-errors',
'--no-abort-on-error'
].join(' ');
}
buildCookiesFlag() {
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 safeExecute(command, silentError = false) {
try {
const result = await exec(command);
return result;
} catch (error) {
if (!silentError) {
console.error(`Command: ${command}`);
console.error(`Error: ${error.message}`);
if (error.stdout) console.error(`Stdout: ${error.stdout}`);
if (error.stderr) console.error(`Stderr: ${error.stderr}`);
}
throw error;
}
}
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() {
if (this.ytDlpCommand) {
return this.ytDlpCommand;
}
if (await this.isYtDlpAvailable()) {
this.ytDlpCommand = 'nice -n 7 yt-dlp';
return this.ytDlpCommand;
}
const fileName = this.detectYtDlpBinaryName();
const filePath = path.join(this.config.ytDlpPath, fileName);
try {
await fs.access(filePath);
this.ytDlpCommand = `nice -n 7 ${filePath}`;
return this.ytDlpCommand;
} catch {
throw new Error('yt-dlp no encontrado. Usa el comando "dla" primero para descargarlo.');
}
}
async processDownloadedFile(message, filePath, customFileName) {
const { mimetype, category } = getFileDetails(filePath);
const fileBuffer = await fs.readFile(filePath);
await message.send(
fileBuffer,
{ fileName: customFileName, mimetype, quoted: message.data },
category
);
await fs.unlink(filePath).catch(() => {});
}
sanitizeFilename(filename) {
return filename.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_');
}
async downloadM3U8(message, m3u8URLs, movieTitle, statusMsg) {
const ytDlpPath = await this.detectYtDlpBinary();
const sessionId = `cuevana_${Date.now()}`;
const outputDir = path.join(this.config.tempDir, sessionId);
const cookiesFlag = this.buildCookiesFlag();
const sanitizedTitle = this.sanitizeFilename(movieTitle);
await this.ensureDirectories();
await fs.mkdir(outputDir, { recursive: true });
await sleep(1000);
await message.send({ key: statusMsg.key, text: `✅ M3U8 encontrados (${m3u8URLs.length}). Iniciando descarga...` }, {}, 'edit');
await sleep(1000);
await message.send({ key: statusMsg.key, text: `⬇️ Descargando ${movieTitle}` }, {}, 'edit');
let downloadSuccess = false;
for (const url of m3u8URLs) {
if (downloadSuccess) break;
try {
const outputTemplate = path.join(outputDir, `${sanitizedTitle}.%(ext)s`);
const command = [
ytDlpPath,
'-f worst',
'--downloader ffmpeg',
'--hls-use-mpegts',
`--max-filesize ${this.config.maxFileSize}`,
this.commonFlags,
cookiesFlag,
`-o "${outputTemplate}"`,
`"${url}"`
].filter(Boolean).join(' ');
await this.safeExecute(command);
const files = await fs.readdir(outputDir);
if (files.length > 0) {
for (const file of files) {
try {
const fileExtension = path.extname(file);
const customFileName = `${sanitizedTitle}${fileExtension}`;
await this.processDownloadedFile(
message,
path.join(outputDir, file),
customFileName
);
downloadSuccess = true;
} catch (processError) {
console.error(`Failed to process file ${file}: ${processError.message}`);
}
}
}
} catch (error) {
console.error(`Error downloading ${url}: ${error.message}`);
}
}
await fs.rm(outputDir, { recursive: true, force: true }).catch(() => {});
if (!downloadSuccess) {
await message.send({ key: statusMsg.key, text: '❌ No se pudo descargar ningún enlace M3U8' }, {}, 'edit');
}
}
}
class M3U8Extractor {
constructor() {
this.m3u8URLs = new Set();
this.config = {
chromiumDataDir: process.env.CHROMIUM_DATA_DIR || '',
chromiumPath: process.env.CHROMIUM_PATH || 'chromium',
userAgent: process.env.CHROMIUM_USERAGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
};
}
async extractM3U8(url, options = {}) {
const { timeout = 60000, waitAfterClick = 8000 } = options;
this.m3u8URLs.clear();
if (!url) throw new Error('URL requerida');
const browser = await chromium.launchPersistentContext(this.config.chromiumDataDir, {
headless: true,
executablePath: this.config.chromiumPath,
viewport: { width: 1366, height: 768 },
args: [
'--no-sandbox',
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-dev-shm-usage',
'--allow-outdated-plugins',
'--disable-logging',
'--disable-breakpad',
'--disable-encryption',
'--disable-machine-id',
'--force-dark-mode',
'--thorium-2024',
'--disable-thorium-icons',
'--remove-tabsearch-button',
'--classic-omnibox',
'--rectangular-tabs',
'--allow-insecure-downloads',
'--allow-insecure-localhost',
'--enable-parallel-downloading',
'--enable-ftp',
'--disable-pull-to-refresh',
'--disable-scrollable-tabstrip',
'--disable-ntp-realbox-cr23-all',
'--disable-ntp-realbox-cr23-consistent-row-height',
'--disable-ntp-realbox-cr23-expanded-state-icons',
'--disable-ntp-realbox-cr23-expanded-state-layout',
'--disable-ntp-realbox-cr23-hover-fill-shape',
'--disable-ntp-realbox-cr23-theming',
'--disable-read-anything-read-aloud',
'--disable-read-anything-with-screen2x',
'--disable-read-anything-with-algorithm',
'--disable-read-anything-images-via-algorithm',
'--disable-read-anything-local-side-panel',
'--disable-chrome-labs',
'--disable-enable-tab-audio-muting',
'--disable-link-preview',
'--autoplay-policy=user-gesture-required',
'--tab-hover-cards=none',
'--close-window-with-last-tab=never',
'--show-avatar-button=always',
'--flag-switches-begin',
'--enable-unsafe-webgpu',
'--ignore-gpu-blocklist',
'--pull-to-refresh=0',
'--revert-from-portable',
'--enable-smooth-scrolling',
'--enable-features=FtpProtocol,ParallelDownloading,Thorium2024,WebContentsForceDark:inversion_method/cielab_based/image_behavior/none/foreground_lightness_threshold/150/background_lightness_threshold/205,Windows11MicaTitlebar',
'--disable-features=ChromeLabs,EnableTabMuting,LinkPreview,NtpRealboxCr23Theming,PdfUseSkiaRenderer,ReadAnythingImagesViaAlgorithm,ReadAnythingReadAloud,ReadAnythingWithAlgorithm,ReadAnythingWithScreen2x,ScrollableTabStrip,SkiaGraphite,UseDMSAAForTiles,TranslateOpenSettings',
'--flag-switches-end',
'--force-disable-tab-outlines',
'--disable-blink-features=AutomationControlled',
'--no-first-run',
'--disable-default-apps',
'--disable-popup-blocking',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--headless',
'--disable-gpu'
]
});
const page = await browser.newPage();
await page.addInitScript(() => {
Object.defineProperty(HTMLMediaElement.prototype, 'play', {
writable: true,
value: function() { return Promise.resolve(); }
});
window.isSpanishContent = function(text) {
if (!text) return false;
const lowerText = text.toLowerCase();
return lowerText.includes('español') || lowerText.includes('latino');
};
window.prioritizeSpanish = function(a, b) {
const aLower = a.toLowerCase();
const bLower = b.toLowerCase();
if (aLower.includes('español latino') && !bLower.includes('español latino')) return -1;
if (!aLower.includes('español latino') && bLower.includes('español latino')) return 1;
if (aLower.includes('latino') && !bLower.includes('latino')) return -1;
if (!aLower.includes('latino') && bLower.includes('latino')) return 1;
if (aLower.includes('español') && !bLower.includes('español')) return -1;
if (!aLower.includes('español') && bLower.includes('español')) return 1;
return 0;
};
});
page.on('request', request => {
const url = request.url();
if (url.includes('.m3u8') || url.includes('m3u8') || url.includes('playlist') || url.includes('master')) {
this.m3u8URLs.add(url);
}
});
page.on('response', response => {
const url = response.url();
const contentType = response.headers()['content-type'] || '';
if (url.includes('.m3u8') || contentType.includes('application/vnd.apple.mpegurl') || contentType.includes('application/x-mpegURL') || (url.includes('playlist') && contentType.includes('text/plain'))) {
this.m3u8URLs.add(url);
}
});
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
await this.scrollToLoadLazy(page);
await page.waitForTimeout(3000);
await this.detectAndExtract(page, waitAfterClick);
} catch (error) {
throw new Error(`Error durante extracción: ${error.message}`);
} finally {
await browser.close();
}
return Array.from(this.m3u8URLs);
}
async detectAndExtract(page, waitAfterClick) {
const strategies = [
() => this.extractFromTabs(page, waitAfterClick),
() => this.extractFromDropdown(page, waitAfterClick),
() => this.extractFromOriginal(page, waitAfterClick),
() => this.extractFromIframes(page, waitAfterClick)
];
for (const strategy of strategies) {
const initialCount = this.m3u8URLs.size;
await strategy();
if (this.m3u8URLs.size > initialCount) break;
}
if (this.m3u8URLs.size === 0) {
await this.searchM3U8InDOM(page);
}
}
async extractFromTabs(page, waitAfterClick) {
const tabs = await page.evaluate(() => {
const tabList = document.querySelector('.TbVideoNv');
if (!tabList) return [];
const items = tabList.querySelectorAll('li[data-url][data-name]');
return Array.from(items)
.filter(item => window.isSpanishContent(item.getAttribute('data-name')))
.map((item, index) => ({
index,
url: item.getAttribute('data-url'),
name: item.getAttribute('data-name')
}))
.sort((a, b) => window.prioritizeSpanish(a.name, b.name));
});
for (const tab of tabs) {
await page.evaluate((index) => {
const tabList = document.querySelector('.TbVideoNv');
const items = tabList.querySelectorAll('li[data-url][data-name]');
const spanishItems = Array.from(items).filter(item =>
window.isSpanishContent(item.getAttribute('data-name'))
);
if (spanishItems[index]) spanishItems[index].click();
}, tab.index);
await page.waitForTimeout(waitAfterClick);
await this.extractFromIframes(page, waitAfterClick);
}
}
async extractFromDropdown(page, waitAfterClick) {
const hasDropdown = await page.evaluate(() => {
return document.querySelector('.repro_dropdown') !== null;
});
if (!hasDropdown) return;
await page.evaluate(() => {
const dropdown = document.querySelector('.repro_dropdown');
if (dropdown) {
const btn = dropdown.querySelector('.repro_dropdown_btn');
const content = dropdown.querySelector('.repro_dropdown_content');
if (btn && content) {
if (content.style.display === 'none' || !content.classList.contains('repro_show')) {
btn.click();
}
}
}
});
await page.waitForTimeout(2000);
const servers = await page.evaluate(() => {
const items = document.querySelectorAll('#episode-options li span[data-id]');
return Array.from(items).map((item, index) => ({
index,
title: item.getAttribute('title'),
id: item.getAttribute('data-id')
}));
});
for (const server of servers) {
await page.evaluate((id) => {
const item = document.querySelector(`#episode-options li span[data-id="${id}"]`);
if (item) item.click();
}, server.id);
await page.waitForTimeout(waitAfterClick);
await this.extractFromIframes(page, waitAfterClick);
}
}
async extractFromOriginal(page, waitAfterClick) {
const expanded = await page.evaluate(() => {
const spanishSection = Array.from(document.querySelectorAll('li.open_submenu')).find(li => {
const spanElement = li.querySelector('._1R6bW_0 span');
return spanElement && window.isSpanishContent(spanElement.textContent);
});
if (spanishSection) {
const menuButton = spanishSection.querySelector('._3CT5n_0');
if (menuButton) {
menuButton.click();
return true;
}
}
return false;
});
if (!expanded) return;
await page.waitForTimeout(2000);
const servers = await page.evaluate(() => {
const spanishSection = Array.from(document.querySelectorAll('li.open_submenu')).find(li => {
const spanElement = li.querySelector('._1R6bW_0 span');
return spanElement && window.isSpanishContent(spanElement.textContent);
});
if (spanishSection) {
const subMenu = spanishSection.querySelector('.sub-tab-lang');
if (subMenu) {
const serverItems = subMenu.querySelectorAll('li.clili[data-tr]');
return Array.from(serverItems).map((item, index) => ({
index,
datatr: item.getAttribute('data-tr'),
name: item.textContent.trim()
}));
}
}
return [];
});
for (const server of servers) {
await page.evaluate((serverIndex) => {
const spanishSection = Array.from(document.querySelectorAll('li.open_submenu')).find(li => {
const spanElement = li.querySelector('._1R6bW_0 span');
return spanElement && window.isSpanishContent(spanElement.textContent);
});
if (spanishSection) {
const subMenu = spanishSection.querySelector('.sub-tab-lang');
if (subMenu) {
const serverItems = subMenu.querySelectorAll('li.clili[data-tr]');
if (serverItems[serverIndex]) {
serverItems[serverIndex].click();
}
}
}
}, server.index);
await page.waitForTimeout(waitAfterClick);
await this.extractFromIframes(page, waitAfterClick);
}
}
async extractFromIframes(page, waitAfterClick) {
const iframes = await page.$$('iframe');
for (const iframe of iframes) {
try {
const isVisible = await iframe.isVisible();
if (!isVisible) continue;
const box = await iframe.boundingBox();
if (box && box.width > 50 && box.height > 50) {
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
await page.mouse.click(centerX, centerY);
await page.waitForTimeout(1000);
await iframe.click({ force: true });
await page.waitForTimeout(waitAfterClick);
}
} catch (error) {
continue;
}
}
}
async scrollToLoadLazy(page) {
await page.evaluate(() => {
return new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if(totalHeight >= scrollHeight){
clearInterval(timer);
resolve();
}
}, 100);
});
});
await page.waitForTimeout(2000);
await page.evaluate(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
await page.waitForTimeout(1000);
}
async searchM3U8InDOM(page) {
const m3u8InDOM = await page.evaluate(() => {
const urls = [];
const regex = /https?:\/\/[^\s"']+\.m3u8[^\s"']*/g;
const htmlContent = document.documentElement.outerHTML;
const matches = htmlContent.match(regex);
if (matches) urls.push(...matches);
for (let key in window) {
try {
const value = window[key];
if (typeof value === 'string' && value.includes('.m3u8')) {
const urlMatches = value.match(regex);
if (urlMatches) urls.push(...urlMatches);
}
} catch (e) {}
}
return [...new Set(urls)];
});
m3u8InDOM.forEach(url => this.m3u8URLs.add(url));
}
}
class CuevanaSearcher {
constructor() {
this.config = {
maxResults: 10,
baseUrl: 'https://wwv.cuevana3.eu',
userAgent: process.env.CHROMIUM_USERAGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
messages: {
usage: 'Uso: cuevana <nombre de película>\nEjemplo: cuevana avengers\n\nUso: cuevana dd <número>\nEjemplo: cuevana dd 1',
searching: '🔍 Buscando películas...',
noResults: 'No se encontraron resultados.',
error: 'Error al procesar.'
}
};
this.headers = {
'User-Agent': this.config.userAgent
};
}
async makeRequest(url) {
return await axios.get(url, { headers: this.headers });
}
async searchMovies(query) {
try {
const response = await this.makeRequest(`${this.config.baseUrl}/search?q=${encodeURIComponent(query)}`);
const $ = cheerio.load(response.data);
const results = [];
$('.MovieList .TPostMv').each((_, element) => {
const $element = $(element);
const title = $element.find('.TPostMv .Title').first().text().trim();
const link = this.config.baseUrl + $element.find('a').attr('href');
if (title && link) {
results.push({ title, link });
}
});
return results;
} catch (error) {
console.error('Error en Cuevana Plugin Buscar:', error);
throw error;
}
}
async getMovieInfo(url) {
try {
const response = await this.makeRequest(url);
const $ = cheerio.load(response.data);
const title = $('h1.Title').text().trim();
const sinopsis = $('.Description p').text().trim();
let genero = '';
$('.TPost .InfoList li').each((i, el) => {
const label = $(el).find('strong').text().trim();
const value = $(el).text().replace(label, '').trim();
if (label.toLowerCase().includes('género') || label.toLowerCase().includes('genero')) {
genero = value;
}
});
return {
title,
genero,
sinopsis,
onlineLink: url
};
} catch (error) {
console.error('Error en Cuevana Plugin:', error);
throw error;
}
}
async sendMovieInfo(message, movieInfo, index) {
try {
let infoMessage = `🎬 *${index + 1} ${movieInfo.title}*\n`;
if (movieInfo.genero) {
infoMessage += `> ${movieInfo.genero}\n`;
}
if (movieInfo.sinopsis) {
infoMessage += `📖 ${movieInfo.sinopsis}\n`;
}
infoMessage += `🌐 ${movieInfo.onlineLink}`;
await message.send(infoMessage, { quoted: message.data });
} catch (error) {
console.error('Error en Cuevana Plugin:', error);
await message.send(`❌ Error al procesar: ${movieInfo.title}`, { quoted: message.data });
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
const cuevanaSearcher = new CuevanaSearcher();
const m3u8Extractor = new M3U8Extractor();
const cuevanaDownloader = new CuevanaDownloader();
bot(
{
pattern: 'cuevana ?(.*)',
fromMe: true,
desc: 'Buscar y descargar películas de Cuevana3',
type: 'search',
},
async (message, match) => {
const input = (match || '').trim();
const chatId = message.jid;
if (!input) {
return await message.send(cuevanaSearcher.config.messages.usage, { quoted: message.data });
}
if (input.startsWith('dd ')) {
const resultIndex = parseInt(input.replace('dd ', '')) - 1;
if (!searchResults.has(chatId)) {
return await message.send('❌ No hay resultados de búsqueda. Usa *cuevana [búsqueda]* primero.', { quoted: message.data });
}
const results = searchResults.get(chatId);
if (resultIndex < 0 || resultIndex >= results.length || !results[resultIndex]) {
return await message.send(`❌ Número inválido o película no disponible. Selecciona entre 1 y ${results.length}.`, { quoted: message.data });
}
const selectedMovie = results[resultIndex];
try {
const statusMsg = await message.send(`🎬 Seleccionado: *${selectedMovie.title}*\n⏳ Extrayendo enlaces M3U8...`, { quoted: message.data });
const m3u8URLs = await m3u8Extractor.extractM3U8(selectedMovie.onlineLink);
if (m3u8URLs.length > 0) {
await cuevanaDownloader.downloadM3U8(message, m3u8URLs, selectedMovie.title, statusMsg);
} else {
await message.send({ key: statusMsg.key, text: '❌ No se encontraron enlaces M3U8 en esta película.' }, {}, 'edit');
}
} catch (error) {
console.error('Error extrayendo M3U8:', error);
await message.send(`❌ Error: ${error.message}`, { quoted: message.data });
}
return;
}
const query = input;
try {
await message.send(cuevanaSearcher.config.messages.searching, { quoted: message.data });
const searchResultsArray = await cuevanaSearcher.searchMovies(query);
if (!searchResultsArray || searchResultsArray.length === 0) {
return await message.send(cuevanaSearcher.config.messages.noResults, { quoted: message.data });
}
const limitedResults = searchResultsArray.slice(0, cuevanaSearcher.config.maxResults);
const moviesWithInfo = [];
for (let i = 0; i < limitedResults.length; i++) {
const movie = limitedResults[i];
try {
const movieInfo = await cuevanaSearcher.getMovieInfo(movie.link);
moviesWithInfo.push(movieInfo);
await cuevanaSearcher.sendMovieInfo(message, movieInfo, i);
if (i < limitedResults.length - 1) {
await cuevanaSearcher.delay(1000);
}
} catch (error) {
console.error(`Error procesando película ${i + 1}:`, error);
await message.send(`❌ Error al obtener información de: ${movie.title}`, { quoted: message.data });
moviesWithInfo.push(null);
}
}
searchResults.set(chatId, moviesWithInfo);
await message.send('💡 Usa: *cuevana dd [número]* para extraer y descargar M3U8', { quoted: message.data });
} catch (error) {
console.error('Error en Cuevana Plugin:', error);
await message.send(cuevanaSearcher.config.messages.error, { quoted: message.data });
}
}
);
module.exports = { cuevanaDownloader, m3u8Extractor, cuevanaSearcher };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment