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