diff --git a/cookies.txt b/cookies.txt index fb15853..aac8b5a 100644 --- a/cookies.txt +++ b/cookies.txt @@ -1,28 +1,19 @@ # Netscape HTTP Cookie File # This file is generated by yt-dlp. Do not edit. -.youtube.com TRUE / TRUE 1788466641 LOGIN_INFO AFmmF2swRAIgbHBdy0Nu7VW6XYN_SFJBR-4n6M7KynMAKD8FVCiu2_YCIFPAXnRpcanD-a5pLw-zHsjfL4bp35ZQamh1Tapjt6oa:QUQ3MjNmeGRLVjJQRUZYWjE5dlFwOC1ndlU4NURpdUpmWmRwbXRhdDljLUpMcFNXSm1Wd2tHckZFZDVDYVFTSGpfR0xyV1BzV3VxeF8tUVVXendNNmdSRG9CVWNsWGZ0Qk1LUXR3NVpoNzlxa1VSc1NKeVp5Y0VtU2hFUXpKM2VteTR2WUJkN2NCVVZSbmpYN1BSRHJUN0ZRNzlFOE52RGR3 .youtube.com TRUE / FALSE 0 PREF f6=40000000&tz=UTC&f5=30000&f7=100&hl=en -.youtube.com TRUE / FALSE 1789360712 SID g.a0007whbriZnp92ClQhGFX1rwjywb8LWXk2yZrUS8uC7oWURQF3Sd14c2pQwMiLB3rascNrdsQACgYKAaASARYSFQHGX2MiOM77ZgpG2e47DuUh739RoRoVAUF8yKpIlf7SEqt5DYDPfeZJvqN20076 -.youtube.com TRUE / TRUE 1789360712 __Secure-1PSIDTS sidts-CjQBBj1CYoqEtR7vcjZa3YzuhXEDXu8mZdx-DrVgiuA5UYtq4KPkL-z9SeVZnMzeSwnjWNS1EAA -.youtube.com TRUE / TRUE 1789360712 __Secure-3PSIDTS sidts-CjQBBj1CYoqEtR7vcjZa3YzuhXEDXu8mZdx-DrVgiuA5UYtq4KPkL-z9SeVZnMzeSwnjWNS1EAA -.youtube.com TRUE / TRUE 1789360712 __Secure-1PSID g.a0007whbriZnp92ClQhGFX1rwjywb8LWXk2yZrUS8uC7oWURQF3Sqg6-JXfQvEMbQba2l_epqgACgYKAQISARYSFQHGX2Mi9OdS6HdW3zGDqyqx9QYKAxoVAUF8yKoBv-iOQqJAWdUR6ChICj4T0076 -.youtube.com TRUE / TRUE 1789360712 __Secure-3PSID g.a0007whbriZnp92ClQhGFX1rwjywb8LWXk2yZrUS8uC7oWURQF3SmoS2rpj7Rpay3oxXJ7IVHQACgYKAW0SARYSFQHGX2MiRd2lm3alPkv2GgGtwPyh9hoVAUF8yKpnv7JHLuHUmmOvg2e2GYRd0076 -.youtube.com TRUE / FALSE 1789360712 HSID AN00cuhnOxnI1f17w -.youtube.com TRUE / TRUE 1789360712 SSID AlVMZlE6lqxQer8Cg -.youtube.com TRUE / FALSE 1789360712 APISID SgFnX0aSre9k_FX5/Aw8YJ4wca6ji8rB31 -.youtube.com TRUE / TRUE 1789360712 SAPISID HoMKo1KjyLfr3o1C/AJ8K1yuWd3MzVH0N3 -.youtube.com TRUE / TRUE 1789360712 __Secure-1PAPISID HoMKo1KjyLfr3o1C/AJ8K1yuWd3MzVH0N3 -.youtube.com TRUE / TRUE 1789360712 __Secure-3PAPISID HoMKo1KjyLfr3o1C/AJ8K1yuWd3MzVH0N3 -.youtube.com TRUE / TRUE 0 wide 0 -.youtube.com TRUE / FALSE 1805673047 SIDCC AKEyXzWXCMWcWmJwFWJhlZCJMOEmbowv1DCozQVJ6ORXi91MiFClMX_mNBbHM4GdIojVsXhtUA0 -.youtube.com TRUE / TRUE 1805673047 __Secure-1PSIDCC AKEyXzXBaczppQRqKwJhYn5sVkEEeYdF8i5w0fp03WaxpCtHHtLNHRMRE2Y3O4GVJTYZZDPChw -.youtube.com TRUE / TRUE 1805673047 __Secure-3PSIDCC AKEyXzU9x9SmOU_4c5pBlBBT-fPCe6K1s5iVDTAGjXSzk18BYPlhZTHn38RkB_JMowjjx5YU4hk -.youtube.com TRUE / TRUE 1789689047 VISITOR_INFO1_LIVE 4S-EyobbTKc -.youtube.com TRUE / TRUE 1789689047 VISITOR_PRIVACY_METADATA CgJNWBIEGgAgNQ%3D%3D -.youtube.com TRUE / TRUE 1789520683 __Secure-YNID 16.YT=DOXLHawAWqdBO4G_RMBOhodR6gaKKfJ5m2e2NuA0j7rZZk6CTQJvsK3e2tsekIy9cDibkro6cHty_5HOp1R9Gc8P3vqPiuU4xhbNyrUbxIrddQZ9MVIsSIKV3e0byDJYiaY88y5e2oM4eqPrh7CG-opo_n75XuinqREZBcYEBm8krJMv4fS-ypePifnI5axSENKA92-7o-qXleObupYB1e_8OinE8a8CycL8mRJXFqO7r1YTnoaC42m3mbfxsFKKVtyZgWQ5hV0-_9wpGDF4T-fTpiJ7hBD6FqCHVcNnK01vd0SH8C8yo8Ma06mPG51dQWOOUDAbV9vbcRX0ahudzQ -.youtube.com TRUE / TRUE 0 YSC D-tG1rAn4Lc -.youtube.com TRUE / TRUE 1789678649 __Secure-ROLLOUT_TOKEN CM--1vTQjqzfpQEQ4raDxc2OkwMY_8_s5PCxkwM%3D -.youtube.com TRUE / TRUE 1837209046 __Secure-YT_TVFAS t=492476&s=2 -.youtube.com TRUE / TRUE 1789689046 DEVICE_INFO ChxOell4T1RnMU1qa3pPRGN3TkRNM05EVXpOZz09ENbd/M0GGODP/M0G -.youtube.com TRUE /tv TRUE 1806969046 __Secure-YT_DERP CLnTgKO6Aw%3D%3D +.youtube.com TRUE / TRUE 1789703947 __Secure-3PAPISID pVtWWgnn8-dTnfzn/A8vyrIMLxHcK-qczX +.youtube.com TRUE / TRUE 1789703947 __Secure-3PSID g.a0008AhbrhAr1a73RYElWotrMA45jV_G3Vbl_lJS3D8q2O56X3OzsASsxRoYivj4Ll_9rBnzKwACgYKAdASARYSFQHGX2MiG2qUab9m_ffKiF-I7GXeHhoVAUF8yKqyhvCJy3evwlIb1Gnm5DEt0076 +.youtube.com TRUE / TRUE 1789709402 __Secure-1PSIDTS sidts-CjQBBj1CYiRV7UVF_NUth-Gfo95O96s1Cs-1NuMHMJPiT7n_HlJPWYuFOcN1oT5CG8TLiIaHEAA +.youtube.com TRUE / TRUE 1789709402 __Secure-3PSIDTS sidts-CjQBBj1CYiRV7UVF_NUth-Gfo95O96s1Cs-1NuMHMJPiT7n_HlJPWYuFOcN1oT5CG8TLiIaHEAA +.youtube.com TRUE / TRUE 1805695434 __Secure-3PSIDCC AKEyXzXoh06KK4RlJVNA9_d9X7nQTUeTyjck38lMhpTSlrPdPM4WJmXwY1Dwcb04bGAEFsfBOA +.youtube.com TRUE / TRUE 1789717979 VISITOR_INFO1_LIVE tao8KQuuHoU +.youtube.com TRUE / TRUE 1789717979 VISITOR_PRIVACY_METADATA CgJNWBIEGgAgGQ%3D%3D +.youtube.com TRUE / TRUE 1789702583 __Secure-YNID 16.YT=vu9HeA2iqnPH-ADjGUgMWcecXN8tAYkSOswDaAH2YJDti3-DJqQNwyMyan0-6fTAnS8DJXEOTQjcx9WcOO7eoZXbiTQIyc06NpWwnZgBks8y8hM1zLBOwLJgUDID6rMU__elM4SZzXDER7cRrHpK8JBH-BwHmrTr9SLuwxjQy07uDTxhr43IC-Xkc4qgUc0sJIHVDChce5CjKSfYK9FXCOoWW1gzxZ2YUJRrKt5UR5rLaLD8wUvEgesYFEen8bNP1FvBE4TGLl_l7N9EiZFvnI7cwIC-nz2jdlYb-AegSCtZzDTin0Jp0036TozvB6nTIYS-aNTwIw2Fo1F6w8vRhQ +.youtube.com TRUE / TRUE 1789702583 __Secure-ROLLOUT_TOKEN CJvoma2RkvyUPhC78ITB9aqTAxjHlq76ybKTAw%3D%3D +.youtube.com TRUE / TRUE 1837237978 __Secure-YT_TVFAS t=492746&s=2 +.youtube.com TRUE / TRUE 1789717978 DEVICE_INFO ChxOell4T1RrME9EVXdNRGd5TXpjNU1ETXlNUT09ENq//s0GGMr9/c0G +.youtube.com TRUE / TRUE 1774166662 GPS 1 +.youtube.com TRUE / TRUE 0 SOCS CAI +.youtube.com TRUE / TRUE 0 YSC 7WQi0NEj8Ag +.youtube.com TRUE /tv TRUE 1806997978 __Secure-YT_DERP CMyH7KS6Aw%3D%3D diff --git a/cookiewarmer.js b/cookiewarmer.js index 9fe36bd..38db51f 100644 --- a/cookiewarmer.js +++ b/cookiewarmer.js @@ -20,9 +20,10 @@ async function exportCookiesFromBotProfile() { console.log('馃殌 Iniciando Chrome con perfil de Bot...'); + const headlessMode = process.env.HEADLESS === 'false' ? false : true; const context = await chromium.launchPersistentContext(userDataDir, { executablePath: '/usr/bin/google-chrome', - headless: true, // Puedes ponerlo en false si quieres ver c贸mo entra a YouTube + headless: headlessMode, // set HEADLESS=false to see the browser and sign in manually args: [ '--no-sandbox', '--disable-setuid-sandbox', @@ -75,3 +76,6 @@ if (require.main === module) { } module.exports = { exportCookiesFromBotProfile }; + +// Backwards-compatible alias used by server code +module.exports.warmAndExportCookies = exportCookiesFromBotProfile; diff --git a/index.js b/index.js index 75c3e9b..db4a0d1 100644 --- a/index.js +++ b/index.js @@ -7,30 +7,11 @@ const app = express(); const PORT = process.env.PORT || 8282; // CONFIGURACI脫N DE RUTAS -const NODE_PATH = '/home/xesar/.nvm/versions/node/v20.19.4/bin/node'; +const NODE_PATH = '/usr/bin/node'; const COOKIES_FILE = path.resolve(__dirname, 'cookies.txt'); -const { warmAndExportCookies } = require('./cookiewarmer'); -// --- CONFIGURACI脫N DE RUTAS Y CONSTANTES (A帽ade esto aqu铆) --- -const COOKIES_PATH = path.resolve(__dirname, 'cookies.txt'); -// --- CONFIGURACI脫N DE MULTER (SUBIDA DE ARCHIVOS) --- -const storage = multer.diskStorage({ - destination: (req, file, cb) => { - const uploadDir = path.resolve(__dirname, 'uploads'); - if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); - cb(null, uploadDir); - }, - filename: (req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`) -}); +const COOKIES_PATH = COOKIES_FILE; +const { warmAndExportCookies, exportCookiesFromBotProfile } = require('./cookiewarmer'); -const upload = multer({ - storage: storage, - fileFilter: (req, file, cb) => { - if (path.extname(file.originalname) !== '.txt') { - return cb(new Error('Solo se permiten archivos .txt'), false); - } - cb(null, true); - } -}); // Funci贸n para sanear cookies (Evita errores de formato de yt-dlp) function sanitizeCookies() { if (fs.existsSync(COOKIES_FILE)) { @@ -44,8 +25,26 @@ function sanitizeCookies() { return false; } -// ENDPOINT 1: /stream/:id -// Devuelve metadatos, miniaturas y la URL de streaming (directa o m3u8) +// --- CONFIGURACI脫N DE MULTER (SUBIDA DE ARCHIVOS) --- +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadDir = path.resolve(__dirname, 'uploads'); + if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); + cb(null, uploadDir); + }, + filename: (req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`) +}); + +const upload = multer({ + storage: storage, + fileFilter: (req, file, cb) => { + if (path.extname(file.originalname).toLowerCase() !== '.txt') { + return cb(new Error('Solo se permiten archivos .txt'), false); + } + cb(null, true); + } +}); + app.get('/stream/:id', (req, res) => { sanitizeCookies(); const videoId = req.params.id; @@ -85,15 +84,242 @@ app.get('/stream/:id', (req, res) => { ], ffmpeg_example: `ffmpeg -re -i "${info.url}" -c copy -f flv rtmp://destino/stream_key` }; - res.json(response); + return res.json(response); } catch (e) { res.status(500).json({ error: "Error parseando JSON de yt-dlp" }); } }); }); -// ENDPOINT 2: /transcript/:id -// Devuelve las miniaturas y el texto completo de la transcripci贸n +// --- Robust /nostream endpoint: busca en vivo y no-vivo usando la l贸gica completa (incluye js-runtime fallback) --- +app.get('/nostream/:id', async (req, res) => { + sanitizeCookies(); + const videoId = req.params.id; + const youtubeUrl = `https://www.youtube.com/watch?v=${videoId}`; + + const args = [ + '--cookies', COOKIES_FILE, + '--no-warnings', + '--dump-json', + '--format', 'bestvideo+bestaudio/best', + '--extractor-args', 'youtube:player_client=tv,web', + youtubeUrl + ]; + + const runYtDlp = (spawnArgs, timeoutMs = 30000) => new Promise((resolve) => { + const p = spawn('yt-dlp', spawnArgs); + let out = ''; + let err = ''; + let killed = false; + const to = setTimeout(() => { killed = true; try { p.kill('SIGKILL'); } catch(e){} }, timeoutMs); + p.stdout.on('data', d => out += d); + p.stderr.on('data', d => err += d); + p.on('close', (code) => { clearTimeout(to); resolve({ code, out, err, killed }); }); + }); + + const probeFormatUrl = (formatId, timeoutMs = 20000) => new Promise((resolve) => { + const runOnce = (args, tms) => new Promise((resolveRun) => { + const p = spawn('yt-dlp', args); + let out = ''; + let err = ''; + const to = setTimeout(() => { try{ p.kill('SIGKILL'); }catch(e){}; resolveRun({ ok: false, out, err }); }, tms); + p.stdout.on('data', d => out += d); + p.stderr.on('data', d => err += d); + p.on('close', (code) => { clearTimeout(to); if (code !== 0) return resolveRun({ ok: false, out, err }); const line = out.split(/\r?\n/).find(Boolean); resolveRun({ ok: true, url: line || null, out, err }); }); + }); + + const baseArgs = ['--cookies', COOKIES_FILE, '--no-warnings', '--get-url', '--format', formatId, youtubeUrl]; + runOnce(baseArgs, timeoutMs).then(r => { + if (r.ok && r.url) return resolve(r.url); + // try forcing JS runtime as a fallback + const jsArgs = ['--cookies', COOKIES_FILE, '--no-warnings', '--js-runtime', `node:${NODE_PATH}`, '--get-url', '--format', formatId, youtubeUrl]; + runOnce(jsArgs, timeoutMs).then(r2 => { + if (r2.ok && r2.url) return resolve(r2.url); + return resolve(null); + }); + }); + }); + + try { + let result = await runYtDlp(args, 30000); + if (result.code !== 0) { + // retry once without js-runtime hints + const altArgs = args.filter(a => !a.startsWith('--js-runtime') && !a.startsWith('node:')); + result = await runYtDlp(altArgs, 30000); + + // If requested format is not available, retry with a safer 'best' format + const errText = (result.err || result.out || '').toString(); + if (/Requested format is not available/i.test(errText) || /Some formats may be missing/i.test(errText)) { + const fmtArgs = altArgs.slice(); + const idx = fmtArgs.findIndex((v, i) => v === '--format'); + if (idx !== -1) { + fmtArgs[idx+1] = 'best'; + } else { + // append format best + fmtArgs.splice(fmtArgs.length-1, 0, '--format', 'best'); + } + const second = await runYtDlp(fmtArgs, 30000); + if (second.code === 0) result = second; // accept this successful result + } + + // If still failing, try to fetch full info (formats list) and pick a merged format + if (result.code !== 0) { + const infoArgs = ['--cookies', COOKIES_FILE, '--no-warnings', '--dump-json', '--extractor-args', 'youtube:player_client=tv,web', youtubeUrl]; + const infoResult = await runYtDlp(infoArgs, 30000); + if (infoResult.code === 0) { + try { + const infoObj = JSON.parse(infoResult.out); + if (Array.isArray(infoObj.formats)) { + const merged = infoObj.formats.filter(f => (f.vcodec && f.vcodec !== 'none') && (f.acodec && f.acodec !== 'none')); + if (merged.length) { + merged.sort((a,b) => (b.tbr||b.bitrate||b.filesize||b.height||0) - (a.tbr||a.bitrate||a.filesize||a.height||0)); + const chosen = merged[0]; + // prefer format_id, otherwise format + const fmtId = chosen.format_id || chosen.format; + if (fmtId) { + const probed = await probeFormatUrl(fmtId, 20000); + if (probed) { + // respond immediately using infoObj and probed url + const urlType = probed.includes('.m3u8') ? 'hls/m3u8' : 'direct'; + return res.json({ + video_id: videoId, + title: infoObj.title, + description: infoObj.description, + is_live: infoObj.is_live || false, + stream_url: probed, + hls_variant_url: infoObj.manifest_url || '', + url_type: urlType, + youtube_url: youtubeUrl, + thumbnails: [ + `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, + `https://img.youtube.com/vi/${videoId}/hqdefault.jpg` + ], + ffmpeg_example: `ffmpeg -re -i "${probed}" -c copy -f flv rtmp://destino/stream_key` + }); + } + } + } + } + } catch (e) { + // ignore parse errors here, we'll fall through to error below + } + } + } + + // Final fallback: try forcing a JS runtime (node) to solve JS challenges + if (result.code !== 0) { + try { + const jsArgs = args.slice(); + if (!jsArgs.includes('--js-runtime')) { + jsArgs.splice(jsArgs.length-1, 0, '--js-runtime', `node:${NODE_PATH}`); + } + const jsResult = await runYtDlp(jsArgs, 40000); + if (jsResult.code === 0) result = jsResult; + } catch (e) { + // ignore and fall through to error + } + } + } + if (result.code !== 0) return res.status(500).json({ error: 'Error al extraer stream', details: result.err || result.out }); + + const info = JSON.parse(result.out); + + function scoreFormat(f) { return (f.tbr || f.bitrate || f.filesize || f.height || 0); } + + // Prefer HLS (m3u8) with v+a + let hlsVariant = info.manifest_url || ''; + if (!hlsVariant && Array.isArray(info.formats)) { + const hlsCandidates = info.formats.filter(f => { + const hasV = f.vcodec && f.vcodec !== 'none'; + const hasA = f.acodec && f.acodec !== 'none'; + const isHls = (f.ext && f.ext.toLowerCase() === 'm3u8') || (f.protocol && String(f.protocol).toLowerCase().includes('m3u8')) || (f.format_note && String(f.format_note).toLowerCase().includes('hls')); + return hasV && hasA && isHls && f.url; + }); + if (hlsCandidates.length) { hlsCandidates.sort((a,b) => scoreFormat(b)-scoreFormat(a)); hlsVariant = hlsCandidates[0].url; } + } + + // Best merged format + let bestMergedFormat = null; + if (Array.isArray(info.formats)) { + const merged = info.formats.filter(f => (f.vcodec && f.vcodec !== 'none') && (f.acodec && f.acodec !== 'none')); + if (merged.length) { merged.sort((a,b) => scoreFormat(b)-scoreFormat(a)); bestMergedFormat = merged[0]; } + } + + let streamUrl = ''; + if (hlsVariant) streamUrl = hlsVariant; + else if (bestMergedFormat && bestMergedFormat.url) streamUrl = bestMergedFormat.url; + else if (bestMergedFormat && bestMergedFormat.format_id) { + const probed = await probeFormatUrl(bestMergedFormat.format_id, 20000); + if (probed) streamUrl = probed; + } + if (!streamUrl) streamUrl = info.url || ''; + + const urlType = streamUrl && streamUrl.includes('.m3u8') ? 'hls/m3u8' : 'direct'; + + res.json({ + video_id: videoId, + title: info.title, + description: info.description, + is_live: info.is_live || false, + stream_url: streamUrl, + hls_variant_url: hlsVariant || '', + url_type: urlType, + youtube_url: youtubeUrl, + thumbnails: [ + `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, + `https://img.youtube.com/vi/${videoId}/hqdefault.jpg` + ], + ffmpeg_example: `ffmpeg -re -i "${streamUrl}" -c copy -f flv rtmp://destino/stream_key` + }); + } catch (err) { + res.status(500).json({ error: 'Error procesando petici贸n', details: err.message }); + } +}); + +// Diagnostic endpoint: returns full yt-dlp JSON (formats list) for a video id +app.get('/formats/:id', async (req, res) => { + const videoId = req.params.id; + const youtubeUrl = `https://www.youtube.com/watch?v=${videoId}`; + + const run = (args, timeoutMs = 20000) => new Promise((resolve) => { + const p = spawn('yt-dlp', args); + let out = ''; + let err = ''; + const to = setTimeout(() => { try { p.kill('SIGKILL'); } catch(e){}; resolve({ code: 124, out, err }); }, timeoutMs); + p.stdout.on('data', d => out += d); + p.stderr.on('data', d => err += d); + p.on('close', (code) => { clearTimeout(to); resolve({ code, out, err }); }); + }); + + // Try several invocations to get diagnostic info + const attempts = [ + ['--cookies', COOKIES_FILE, '--no-warnings', '--dump-json', '--extractor-args', 'youtube:player_client=tv,web', youtubeUrl], + ['--no-warnings', '--dump-json', youtubeUrl], + ['--no-warnings', '--list-formats', youtubeUrl] + ]; + + for (const args of attempts) { + const r = await run(args, 20000); + if (r.code === 0) { + // success + if (args.includes('--dump-json')) { + try { + const info = JSON.parse(r.out); + return res.json(info); + } catch (e) { + return res.status(500).json({ error: 'parse error', details: e.message, raw: r.out.slice(0,2000) }); + } + } + // list-formats or other raw output + return res.json({ raw: r.out }); + } + // If this attempt failed, capture last stderr for diagnosis and continue + var lastErr = r.err || r.out || ''; + } + + return res.status(500).json({ error: 'yt-dlp failed to list formats', details: lastErr }); +}); + app.get('/transcript/:id', (req, res) => { sanitizeCookies(); const videoId = req.params.id; @@ -114,9 +340,6 @@ app.get('/transcript/:id', (req, res) => { child.stdout.on('data', (data) => output += data); child.on('close', (code) => { - // Nota: yt-dlp devuelve las URLs de los subt铆tulos. - // Para obtener el "format_text" como tu JSON, se requiere procesar el .vtt o .ttml - // Aqu铆 enviamos la estructura base solicitada. res.json({ video_id: videoId, thumbnails: [ @@ -196,7 +419,7 @@ app.post('/upload-cookies', upload.single('cookies'), (req, res) => { }); /** * Endpoint para forzar la actualizaci贸n de cookies.txt - * Uso: http://localhost:3000/refresh-cookies?id=PzWTM7YvGag + * Uso: http://localhost:8282/refresh-cookies */ app.get('/refresh-cookies', async (req, res) => { try {