tubapi-node/index.js
2026-03-21 19:54:23 -07:00

245 lines
9.6 KiB
JavaScript

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(`
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin | Enlace Directo News</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
.drop-zone { border: 2px dashed #334155; transition: all 0.3s; }
.drop-zone.active { border-color: #38bdf8; background: #1e293b; }
.advice-card { background: linear-gradient(145deg, rgba(245, 158, 11, 0.1), rgba(245, 158, 11, 0.05)); }
</style>
</head>
<body class="flex items-center justify-center min-h-screen p-4">
<div class="w-full max-w-md bg-slate-800 p-8 rounded-2xl shadow-2xl border border-slate-700">
<header class="text-center mb-8">
<div class="inline-block p-3 bg-sky-500/10 rounded-full mb-4">
<i class="fa-solid fa-bolt-lightning text-sky-400 text-2xl"></i>
</div>
<h1 class="text-2xl font-bold text-white">Enlace Directo News</h1>
<p class="text-slate-400 text-sm">Gestor de Sesión de YouTube</p>
</header>
<form id="uploadForm" class="space-y-6">
<div id="dropZone" class="drop-zone p-10 rounded-xl text-center cursor-pointer group">
<i class="fa-solid fa-file-code text-4xl mb-4 text-slate-500 group-hover:text-sky-400 transition-colors"></i>
<p class="text-sm text-slate-300">Arrastra tu <b>cookies.txt</b> aquí<br><span class="text-slate-500 text-xs">o haz clic para buscar</span></p>
<input type="file" id="fileInput" name="cookies" accept=".txt" class="hidden">
</div>
<div class="advice-card border border-amber-500/20 rounded-xl p-4 flex items-start gap-3">
<div class="text-amber-500 mt-0.5">
<i class="fa-solid fa-circle-info"></i>
</div>
<div class="flex-1">
<p class="text-slate-300 text-[13px] leading-snug">
<strong class="text-amber-400">Tip Pro:</strong> Mira un video por 15 segundos antes de exportar. Esto "calienta" las cookies y reduce bloqueos de bot.
</p>
</div>
</div>
<div id="fileInfo" class="hidden bg-slate-900/50 p-3 rounded-lg text-xs flex items-center justify-between border border-slate-700">
<span id="fileName" class="truncate pr-4 text-sky-200 font-mono"></span>
<button type="button" id="clearBtn" class="text-rose-400 hover:text-rose-300"><i class="fa-solid fa-trash"></i></button>
</div>
<button type="submit" id="submitBtn" class="w-full bg-sky-600 hover:bg-sky-500 text-white font-semibold py-3 rounded-xl transition-all shadow-lg shadow-sky-900/20 active:scale-95">
<i class="fa-solid fa-sync mr-2"></i>Actualizar Cookies
</button>
</form>
<div id="status" class="mt-6 text-center text-sm font-medium hidden p-3 rounded-lg"></div>
<footer class="mt-8 pt-6 border-t border-slate-700 text-center">
<p class="text-slate-500 text-[10px] uppercase tracking-widest font-bold">Bot Profile v2.0 | Ubuntu Environment</p>
</footer>
</div>
<script> ... </script>
</body>
</html>
`);
});
// --- 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
------------------------------------------
`);
});