UNPKG

@soymaycol/maytube

Version:

📥 Módulo para descargar videos o audios de YouTube fácilmente usando dos APIs secretas

497 lines (423 loc) 15.5 kB
import crypto from "crypto"; import axios from 'axios'; // Configuración global para mejorar rendimiento const globalConfig = { timeout: 15000, // Reducido de 15000 a 8000ms maxRetries: 2, concurrentLimit: 3, cacheTimeout: 300000, // 5 minutos headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'cross-site' } }; // Cache simple para evitar requests repetidos const cache = new Map(); // Función de validación profunda para evitar archivos corruptos async function deepValidate(url) { if (!url || typeof url !== 'string') return false; // Validaciones básicas rápidas if (url.includes('googlevideo.com') || url.includes('googleusercontent.com')) { console.log('⚠️ Enlace de Google Video rechazado'); return false; } if (!url.startsWith('http://') && !url.startsWith('https://')) { console.log('⚠️ URL inválida'); return false; } // Cache check const cacheKey = `validate_${url}`; if (cache.has(cacheKey)) { const cached = cache.get(cacheKey); if (Date.now() - cached.timestamp < globalConfig.cacheTimeout) { return cached.result; } cache.delete(cacheKey); } try { // Paso 1: Verificar headers const headResponse = await axios.head(url, { timeout: 5000, headers: globalConfig.headers, maxRedirects: 5 }); const contentLength = headResponse.headers['content-length']; const contentType = headResponse.headers['content-type']; // Validar tamaño mínimo if (contentLength && parseInt(contentLength) < 5000) { console.log('⚠️ Archivo muy pequeño:', contentLength); return false; } // Validar tipo de contenido if (contentType) { const invalidTypes = ['text/html', 'application/json', 'text/plain', 'text/xml']; if (invalidTypes.some(type => contentType.includes(type))) { console.log('⚠️ Tipo de contenido inválido:', contentType); return false; } } // Paso 2: Verificar primeros bytes del archivo (magic numbers) const sampleResponse = await axios.get(url, { timeout: 8000, headers: { ...globalConfig.headers, 'Range': 'bytes=0-2047' // Solo primeros 2KB }, responseType: 'arraybuffer', maxRedirects: 5 }); const buffer = Buffer.from(sampleResponse.data); const result = validateFileSignature(buffer, contentType); // Cachear resultado cache.set(cacheKey, { result, timestamp: Date.now() }); return result; } catch (error) { console.log('⚠️ Error en validación:', error.message); cache.set(cacheKey, { result: false, timestamp: Date.now() }); return false; } } // Función para validar firmas de archivo (magic numbers) function validateFileSignature(buffer, contentType) { if (!buffer || buffer.length < 4) return false; const hex = buffer.toString('hex').toUpperCase(); // Firmas de archivos de audio válidos const audioSignatures = [ 'FFFB', 'FFF3', 'FFF2', // MP3 '494433', // MP3 con ID3 '4F676753', // OGG '664C6143', // FLAC '52494646', // WAV/AVI (RIFF) '1A45DFA3', // WebM/MKV '00000018667479706D703421', // MP4 (ftyp) '00000020667479706D703421', // MP4 (ftyp) '667479706D703421', // MP4 (ftyp) '3026B2758E66CF11', // WMA '4D546864' // MIDI ]; // Verificar si coincide con alguna firma válida for (const signature of audioSignatures) { if (hex.startsWith(signature)) { console.log(`✅ Firma de archivo válida detectada: ${signature}`); return true; } } // Si no hay firma conocida pero el content-type es de audio/video, permitir if (contentType && (contentType.includes('audio/') || contentType.includes('video/') || contentType.includes('application/octet-stream'))) { console.log('⚠️ Firma desconocida pero content-type válido'); return true; } // Verificar si es HTML (página de error) if (hex.startsWith('3C21444F43545950') || hex.startsWith('3C68746D6C') || hex.startsWith('3C48544D4C')) { console.log('⚠️ HTML detectado en lugar de archivo multimedia'); return false; } console.log('⚠️ Firma de archivo no reconocida:', hex.substring(0, 16)); return false; } // Función de request optimizada con reintentos async function makeRequest(url, options = {}) { const config = { timeout: globalConfig.timeout, headers: globalConfig.headers, maxRedirects: 5, ...options }; for (let attempt = 0; attempt < globalConfig.maxRetries; attempt++) { try { const response = await axios(url, config); return { success: true, data: response.data, headers: response.headers }; } catch (error) { if (attempt === globalConfig.maxRetries - 1) { return { success: false, error: error.message }; } // Esperar antes del siguiente intento await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1))); } } } // API optimizada: Sylphy (más rápida y confiable) async function ytSylphy(url, format = 'mp3') { try { const quality = format === 'mp3' ? '128' : '480'; const apiUrl = `https://ytdl.sylphy.xyz/dl/${format}?url=${encodeURIComponent(url)}&quality=${quality}`; const response = await makeRequest(apiUrl); if (!response.success) { return { status: false, error: response.error }; } const data = response.data; if (!data.success || !data.data?.dl_url) { return { status: false, error: "No se pudo obtener el enlace de descarga" }; } const downloadUrl = data.data.dl_url; const isValid = await deepValidate(downloadUrl); if (!isValid) { return { status: false, error: "Archivo no válido" }; } return { status: true, result: { title: data.data.title || "Unknown", type: format === 'mp3' ? 'audio' : 'video', format: format, download: downloadUrl, size: data.data.size_mb ? `${data.data.size_mb} MB` : 'Unknown', quality: quality, duration: data.data.duration || 'Unknown' } }; } catch (error) { return { status: false, error: error.message }; } } // API Vreden optimizada async function ytVreden(url, format = 'mp3') { try { const endpoint = format === 'mp3' ? 'ytmp3' : 'ytmp4'; const apiUrl = `https://api.vreden.my.id/api/${endpoint}?url=${encodeURIComponent(url)}`; const response = await makeRequest(apiUrl); if (!response.success) { return { status: false, error: response.error }; } const data = response.data; if (data.status !== 200 || !data.result?.status) { return { status: false, error: "API response error" }; } const downloadUrl = data.result.download.url; const isValid = await deepValidate(downloadUrl); if (!isValid) { return { status: false, error: "Archivo no válido" }; } return { status: true, result: { title: data.result.metadata.title, type: format === 'mp3' ? 'audio' : 'video', format: format, download: downloadUrl, size: 'Unknown', quality: data.result.download.quality, thumbnail: data.result.metadata.thumbnail, duration: data.result.metadata.timestamp, author: data.result.metadata.author.name } }; } catch (error) { return { status: false, error: error.message }; } } // API Delirius optimizada async function ytDelirius(url, format = 'mp3') { try { const endpoint = format === 'mp3' ? 'ytmp3' : 'ytmp4'; const apiUrl = `https://delirius-apiofc.vercel.app/download/${endpoint}?url=${encodeURIComponent(url)}`; const response = await makeRequest(apiUrl); if (!response.success) { return { status: false, error: response.error }; } const data = response.data; if (!data.status || !data.data?.download?.url) { return { status: false, error: "API response error" }; } const downloadUrl = data.data.download.url; const isValid = await deepValidate(downloadUrl); if (!isValid) { return { status: false, error: "Archivo no válido" }; } return { status: true, result: { title: data.data.title, type: format === 'mp3' ? 'audio' : 'video', format: format, download: downloadUrl, size: data.data.download.size, quality: data.data.download.quality, thumbnail: data.data.image, duration: data.data.duration, author: data.data.author } }; } catch (error) { return { status: false, error: error.message }; } } // API SaveTube optimizada async function ytSaveTube(url, format = 'mp3') { const apiBase = "https://media.savetube.me/api"; const apiCDN = "/random-cdn"; const apiInfo = "/v2/info"; const apiDownload = "/download"; const decryptData = async (enc) => { try { const key = Buffer.from('C5D58EF67A7584E4A29F6C35BBC4EB12', 'hex'); const data = Buffer.from(enc, 'base64'); const iv = data.slice(0, 16); const content = data.slice(16); const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); let decrypted = decipher.update(content); decrypted = Buffer.concat([decrypted, decipher.final()]); return JSON.parse(decrypted.toString()); } catch (error) { return null; } }; const youtubeID = url.match(/(?:youtu.be\/|youtube.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([a-zA-Z0-9_-]{11})/); if (!youtubeID) return { status: false, error: "ID de video inválido" }; try { // Obtener CDN const cdnResponse = await makeRequest(`${apiBase}${apiCDN}`, { method: 'GET' }); if (!cdnResponse.success) return { status: false, error: cdnResponse.error }; const cdn = cdnResponse.data.cdn; // Obtener info del video const infoResponse = await makeRequest(`https://${cdn}${apiInfo}`, { method: 'POST', data: { url: `https://www.youtube.com/watch?v=${youtubeID[1]}` } }); if (!infoResponse.success) return { status: false, error: infoResponse.error }; const decrypted = await decryptData(infoResponse.data.data); if (!decrypted) return { status: false, error: "Error al descifrar datos" }; // Intentar descarga con la mejor calidad disponible const qualities = format === 'mp3' ? ['128'] : ['720', '480', '360']; for (const quality of qualities) { try { const downloadResponse = await makeRequest(`https://${cdn}${apiDownload}`, { method: 'POST', data: { id: youtubeID[1], downloadType: format === 'mp3' ? 'audio' : 'video', quality: quality, key: decrypted.key } }); if (downloadResponse.success && downloadResponse.data.data?.downloadUrl) { const downloadUrl = downloadResponse.data.data.downloadUrl; const isValid = await deepValidate(downloadUrl); if (isValid) { return { status: true, result: { title: decrypted.title || "Unknown", type: format === 'mp3' ? 'audio' : 'video', format: format, download: downloadUrl, size: 'Unknown', quality: quality } }; } } } catch (error) { continue; } } return { status: false, error: "No se pudo obtener enlace de descarga" }; } catch (error) { return { status: false, error: error.message }; } } // Función principal optimizada para MP3 async function yta(url) { console.log('🎵 Iniciando descarga MP3...'); // APIs ordenadas por velocidad y confiabilidad const apis = [ { name: 'Sylphy', func: (url) => ytSylphy(url, 'mp3') }, { name: 'Vreden', func: (url) => ytVreden(url, 'mp3') }, { name: 'Delirius', func: (url) => ytDelirius(url, 'mp3') }, { name: 'SaveTube', func: (url) => ytSaveTube(url, 'mp3') } ]; // Intentar APIs en paralelo limitado para mayor velocidad const results = await Promise.allSettled( apis.slice(0, 2).map(async (api) => { console.log(`🔄 Probando API: ${api.name}`); const result = await api.func(url); if (result.status) { console.log(`✅ Éxito con ${api.name}`); return { ...result, apiName: api.name }; } throw new Error(`${api.name}: ${result.error}`); }) ); // Retornar el primer resultado exitoso for (const result of results) { if (result.status === 'fulfilled') { return result.value; } } // Si fallan las primeras 2, intentar las restantes secuencialmente for (const api of apis.slice(2)) { try { console.log(`🔄 Probando API: ${api.name}`); const result = await api.func(url); if (result.status) { console.log(`✅ Éxito con ${api.name}`); return { ...result, apiName: api.name }; } } catch (error) { console.log(`❌ Error en ${api.name}: ${error.message}`); } } return { status: false, error: 'Todas las APIs fallaron. Intenta nuevamente en unos minutos.' }; } // Función principal optimizada para MP4 async function ytv(url) { console.log('🎬 Iniciando descarga MP4...'); // APIs ordenadas por velocidad y confiabilidad const apis = [ { name: 'Sylphy', func: (url) => ytSylphy(url, 'mp4') }, { name: 'Vreden', func: (url) => ytVreden(url, 'mp4') }, { name: 'Delirius', func: (url) => ytDelirius(url, 'mp4') }, { name: 'SaveTube', func: (url) => ytSaveTube(url, 'mp4') } ]; // Intentar APIs en paralelo limitado para mayor velocidad const results = await Promise.allSettled( apis.slice(0, 2).map(async (api) => { console.log(`🔄 Probando API: ${api.name}`); const result = await api.func(url); if (result.status) { console.log(`✅ Éxito con ${api.name}`); return { ...result, apiName: api.name }; } throw new Error(`${api.name}: ${result.error}`); }) ); // Retornar el primer resultado exitoso for (const result of results) { if (result.status === 'fulfilled') { return result.value; } } // Si fallan las primeras 2, intentar las restantes secuencialmente for (const api of apis.slice(2)) { try { console.log(`🔄 Probando API: ${api.name}`); const result = await api.func(url); if (result.status) { console.log(`✅ Éxito con ${api.name}`); return { ...result, apiName: api.name }; } } catch (error) { console.log(`❌ Error en ${api.name}: ${error.message}`); } } return { status: false, error: 'Todas las APIs fallaron. Intenta nuevamente en unos minutos.' }; } // Función de limpieza de cache (opcional) function clearCache() { cache.clear(); console.log('🧹 Cache limpiado'); } // Exportar funciones export { yta, ytv, clearCache };