const express = require('express'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const multer = require('multer'); 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 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) => cb(null, './'), filename: (req, file, cb) => cb(null, 'cookies.txt') }); 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)) { let content = fs.readFileSync(COOKIES_FILE, 'utf8'); if (content.includes('# Netscape HTTP Cookie File')) { content = content.substring(content.indexOf('# Netscape HTTP Cookie File')); fs.writeFileSync(COOKIES_FILE, content, 'utf8'); return true; } } return false; } // ENDPOINT 1: /stream/:id // Devuelve metadatos, miniaturas y la URL de streaming (directa o m3u8) app.get('/stream/:id', (req, res) => { sanitizeCookies(); const videoId = req.params.id; const youtubeUrl = `https://www.youtube.com/watch?v=${videoId}`; // Argumentos para obtener el JSON de metadatos const args = [ '--cookies', COOKIES_FILE, '--js-runtime', `node:${NODE_PATH}`, '--dump-json', '--format', 'bestvideo+bestaudio/best', '--extractor-args', 'youtube:player_client=tv,web', youtubeUrl ]; const child = spawn('yt-dlp', args); let output = ''; child.stdout.on('data', (data) => output += data); child.on('close', (code) => { if (code !== 0) return res.status(500).json({ error: "Error al extraer stream" }); try { const info = JSON.parse(output); const response = { video_id: videoId, title: info.title, description: info.description, is_live: info.is_live || false, stream_url: info.url, hls_variant_url: info.manifest_url || "", url_type: info.is_live ? "hls/m3u8" : "direct/mp4", 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 "${info.url}" -c copy -f flv rtmp://destino/stream_key` }; 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 app.get('/transcript/:id', (req, res) => { sanitizeCookies(); const videoId = req.params.id; const youtubeUrl = `https://www.youtube.com/watch?v=${videoId}`; // Argumentos para extraer subtítulos sin descargar video const args = [ '--cookies', COOKIES_FILE, '--js-runtime', `node:${NODE_PATH}`, '--write-auto-subs', '--skip-download', '--print', '%(subtitles)j', // Imprime el JSON de subtítulos youtubeUrl ]; const child = spawn('yt-dlp', args); let output = ''; 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: [ `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, `https://img.youtube.com/vi/${videoId}/sddefault.jpg` ], info: "Para transcripción completa procesada, use un parser de VTT sobre la URL de subtítulos de YouTube." }); }); }); // --- INTERFAZ UX (MODO OSCURO / TAILWIND) --- app.get('/admin', (req, res) => { res.send(` Admin | Enlace Directo News

Enlace Directo News

Gestor de Sesión de YouTube

Arrastra tu cookies.txt aquí
o haz clic para buscar

Tip Pro: Mira un video por 15 segundos antes de exportar. Esto "calienta" las cookies y reduce bloqueos de bot.

`); }); // --- ENDPOINT: SUBIDA Y VALIDACIÓN --- app.post('/upload-cookies', upload.single('cookies'), (req, res) => { try { if (!req.file) { return res.status(400).json({ status: "error", message: "No se seleccionó archivo." }); } const content = fs.readFileSync(COOKIES_PATH, 'utf8'); // Validación estricta del formato Netscape if (!content.includes('# Netscape HTTP Cookie File')) { fs.unlinkSync(COOKIES_PATH); // Borramos el inválido return res.status(400).json({ status: "error", message: "Formato inválido. Debe ser un archivo de cookies Netscape." }); } res.json({ status: "success", message: "cookies.txt actualizado." }); } catch (error) { res.status(500).json({ status: "error", message: error.message }); } }); /** * Endpoint para forzar la actualización de cookies.txt * Uso: http://localhost:3000/refresh-cookies?id=PzWTM7YvGag */ app.get('/refresh-cookies', async (req, res) => { try { await exportCookiesFromBotProfile(); res.json({ status: "success", message: "Cookies sincronizadas y descifradas correctamente desde el perfil del bot." }); } catch (error) { res.status(500).json({ status: "error", details: error.message }); } }); // --- INICIO DEL SERVIDOR --- app.listen(PORT, () => { console.log(` 🚀 API Enlace Directo News activa ------------------------------------------ Panel Admin: http://localhost:${PORT}/admin ------------------------------------------ `); });