Compare commits
3 Commits
5e55dbadd7
...
fcad34cd93
| Author | SHA1 | Date | |
|---|---|---|---|
| fcad34cd93 | |||
| 38ce6dfea4 | |||
| b0e088b9de |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
||||
8
README.md
Normal file
8
README.md
Normal file
@ -0,0 +1,8 @@
|
||||
Frontend build (Vite)
|
||||
|
||||
- Development: run `npm run frontend:dev` in the project root, which starts Vite. Open the dev server URL printed by Vite (usually http://localhost:5173) and the backend at `http://localhost:8282`.
|
||||
- Build: run `npm run frontend:build` which outputs files to `dist/`. The Express server will serve `dist/index.html` at `/admin` once `dist/` exists.
|
||||
|
||||
Notes:
|
||||
- Install dev dependencies with `npm install` before running the scripts.
|
||||
- The Vite build includes `@vitejs/plugin-legacy` for broader browser support.
|
||||
20
client/index.html
Normal file
20
client/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!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>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/lucide@latest/dist/lucide.min.js"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script>window.addEventListener('load', ()=>{ if(window.lucide) lucide.replace() })</script>
|
||||
</body>
|
||||
</html>
|
||||
169
client/src/main.js
Normal file
169
client/src/main.js
Normal file
@ -0,0 +1,169 @@
|
||||
import './styles.css'
|
||||
|
||||
function createNodeFromHTML(html) {
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = html.trim()
|
||||
return template.content.firstChild
|
||||
}
|
||||
|
||||
const markup = `
|
||||
<div class="min-h-screen bg-[#09090b] text-zinc-100 flex items-center justify-center p-6 font-sans">
|
||||
<div class="w-full max-w-md bg-zinc-900/50 border border-zinc-800 backdrop-blur-xl rounded-2xl p-8 shadow-2xl">
|
||||
|
||||
<header class="mb-8 text-center">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-emerald-500/10 text-emerald-500 mb-4 border border-emerald-500/20">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="m9 12 2 2 4-4"/></svg>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold tracking-tight">Enlace Directo News</h1>
|
||||
<p class="text-zinc-500 text-sm mt-1">Panel de administración de sesiones</p>
|
||||
</header>
|
||||
|
||||
<form id="uploadForm" class="space-y-6">
|
||||
<div id="dropZone" class="group relative border-2 border-dashed border-zinc-800 hover:border-emerald-500/50 hover:bg-emerald-500/[0.02] rounded-xl p-8 transition-all duration-200 cursor-pointer text-center">
|
||||
<input id="fileInput" type="file" name="cookies" accept=".txt" hidden />
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="p-3 rounded-full bg-zinc-800 group-hover:bg-emerald-500/10 group-hover:text-emerald-500 transition-colors mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-zinc-300">Arrastra tu <span class="text-emerald-500 text-xs font-mono">cookies.txt</span></p>
|
||||
<p class="text-xs text-zinc-500 mt-1">o haz clic para explorar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fileInfo" class="hidden flex items-center justify-between bg-zinc-800/50 border border-zinc-700 p-3 rounded-lg animate-in fade-in slide-in-from-bottom-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="text-emerald-500" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
<span id="fileName" class="text-xs font-medium truncate max-w-[180px]"></span>
|
||||
</div>
|
||||
<button id="clearBtn" type="button" class="text-xs text-zinc-500 hover:text-red-400 transition-colors font-semibold px-2">Remover</button>
|
||||
</div>
|
||||
|
||||
<div id="progressContainer" class="hidden space-y-2 mb-6">
|
||||
<div class="flex justify-between text-[10px] uppercase tracking-wider text-zinc-500 font-bold">
|
||||
<span>Subiendo archivo...</span>
|
||||
<span id="progressPercent">0%</span>
|
||||
</div>
|
||||
<div class="h-1.5 w-full bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div id="progressBar" class="h-full bg-emerald-500 w-0 transition-all duration-300 ease-out shadow-[0_0_8px_rgba(16,185,129,0.5)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="submitBtn" type="submit" class="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-semibold py-3 rounded-xl transition-all shadow-lg shadow-emerald-900/20 active:scale-[0.98]">
|
||||
Actualizar Cookies
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="status" class="hidden mt-6 p-3 rounded-lg text-sm text-center border animate-in zoom-in-95"></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const root = document.getElementById('app')
|
||||
root.appendChild(createNodeFromHTML(markup))
|
||||
|
||||
const dropZone = document.getElementById('dropZone')
|
||||
const fileInput = document.getElementById('fileInput')
|
||||
const uploadForm = document.getElementById('uploadForm')
|
||||
const fileInfo = document.getElementById('fileInfo')
|
||||
const fileName = document.getElementById('fileName')
|
||||
const clearBtn = document.getElementById('clearBtn')
|
||||
const status = document.getElementById('status')
|
||||
const submitBtn = document.getElementById('submitBtn')
|
||||
const progressContainer = document.getElementById('progressContainer')
|
||||
const progressBar = document.getElementById('progressBar')
|
||||
const progressPercent = document.getElementById('progressPercent')
|
||||
|
||||
function showStatus(text, ok = true) {
|
||||
status.textContent = text
|
||||
status.classList.remove('hidden')
|
||||
// Limpiamos clases previas
|
||||
status.className = 'mt-6 p-3 rounded-lg text-sm text-center border animate-in zoom-in-95'
|
||||
if (ok) {
|
||||
status.classList.add('bg-emerald-500/10', 'text-emerald-400', 'border-emerald-500/20')
|
||||
} else {
|
||||
status.classList.add('bg-red-500/10', 'text-red-400', 'border-red-500/20')
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileInfo() {
|
||||
if (fileInput.files && fileInput.files[0]) {
|
||||
fileInfo.classList.remove('hidden')
|
||||
fileName.textContent = fileInput.files[0].name
|
||||
} else {
|
||||
fileInfo.classList.add('hidden')
|
||||
fileName.textContent = ''
|
||||
}
|
||||
}
|
||||
|
||||
dropZone.addEventListener('click', () => fileInput.click())
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('active') })
|
||||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('active'))
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault()
|
||||
dropZone.classList.remove('active')
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length) fileInput.files = e.dataTransfer.files
|
||||
updateFileInfo()
|
||||
})
|
||||
|
||||
fileInput.addEventListener('change', updateFileInfo)
|
||||
clearBtn.addEventListener('click', () => { fileInput.value = ''; updateFileInfo() })
|
||||
|
||||
uploadForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!fileInput.files || !fileInput.files[0]) {
|
||||
return showStatus('Selecciona un archivo .txt primero', false)
|
||||
}
|
||||
|
||||
const file = fileInput.files[0]
|
||||
const fd = new FormData()
|
||||
fd.append('cookies', file)
|
||||
|
||||
// Reset UI
|
||||
status.classList.add('hidden')
|
||||
progressContainer.classList.remove('hidden')
|
||||
progressBar.style.width = '0%'
|
||||
progressPercent.textContent = '0%'
|
||||
submitBtn.disabled = true
|
||||
submitBtn.classList.add('opacity-50', 'cursor-not-allowed')
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.upload.addEventListener('progress', (ev) => {
|
||||
if (ev.lengthComputable) {
|
||||
const percent = Math.round((ev.loaded / ev.total) * 100)
|
||||
progressBar.style.width = percent + '%'
|
||||
progressPercent.textContent = percent + '%'
|
||||
}
|
||||
})
|
||||
|
||||
xhr.onload = () => {
|
||||
submitBtn.disabled = false
|
||||
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed')
|
||||
|
||||
setTimeout(() => {
|
||||
progressContainer.classList.add('hidden')
|
||||
try {
|
||||
const res = JSON.parse(xhr.responseText)
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
showStatus(res.message || 'Cookies actualizadas con éxito')
|
||||
} else {
|
||||
showStatus(res.message || 'Error en el servidor', false)
|
||||
}
|
||||
} catch (err) {
|
||||
showStatus('Error al procesar respuesta', false)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
xhr.onerror = () => {
|
||||
submitBtn.disabled = false
|
||||
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed')
|
||||
progressContainer.classList.add('hidden')
|
||||
showStatus('Error de conexión a la red', false)
|
||||
}
|
||||
|
||||
xhr.open('POST', '/upload-cookies')
|
||||
xhr.send(fd)
|
||||
})
|
||||
13
client/src/styles.css
Normal file
13
client/src/styles.css
Normal file
@ -0,0 +1,13 @@
|
||||
:root{--bg:#0f172a;--card:#0b1220;--muted:#94a3b8;--accent:#06b6d4}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:Inter,system-ui,sans-serif;background:var(--bg);color:#f8fafc}
|
||||
.container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
|
||||
.card{width:100%;max-width:720px;background:#0f172a;border:1px solid rgba(255,255,255,0.04);padding:28px;border-radius:12px}
|
||||
header h1{margin:0;font-size:20px}
|
||||
.muted{color:var(--muted);font-size:12px}
|
||||
.drop-zone{border:2px dashed rgba(148,163,184,0.08);padding:24px;border-radius:8px;text-align:center;cursor:pointer}
|
||||
.drop-zone.active{border-color:var(--accent);background:#071127}
|
||||
.file-info{display:flex;justify-content:space-between;align-items:center;padding:8px;margin-top:12px;border-radius:6px;background:#071127}
|
||||
.hidden{display:none}
|
||||
button{background:var(--accent);color:#042f2e;padding:10px 14px;border-radius:8px;border:0;cursor:pointer}
|
||||
.status{margin-top:12px;padding:10px;border-radius:8px}
|
||||
39
cookies.txt
39
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
|
||||
|
||||
@ -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;
|
||||
|
||||
399
index.js
399
index.js
@ -7,26 +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) => cb(null, './'),
|
||||
filename: (req, file, cb) => cb(null, 'cookies.txt')
|
||||
});
|
||||
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)) {
|
||||
@ -40,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;
|
||||
@ -81,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;
|
||||
@ -110,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: [
|
||||
@ -125,74 +352,33 @@ app.get('/transcript/:id', (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
// --- 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>
|
||||
`);
|
||||
});
|
||||
// --- INTERFAZ UX (MODO OSCURO / TAILWIND) ---
|
||||
// Serve a built frontend from `dist` when available. During development run the Vite dev server.
|
||||
const distPath = path.join(__dirname, 'dist');
|
||||
if (fs.existsSync(distPath)) {
|
||||
app.use(express.static(distPath));
|
||||
app.get('/admin', (req, res) => res.sendFile(path.join(distPath, 'index.html')));
|
||||
} else {
|
||||
app.get('/admin', (req, res) => {
|
||||
res.send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin - Build missing</title>
|
||||
</head>
|
||||
<body style="background:#0f172a;color:#f8fafc;font-family:Inter,system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;">
|
||||
<div style="text-align:center;max-width:640px;padding:24px;">
|
||||
<h1>Frontend not built</h1>
|
||||
<p>Run <code>npm run build</code> to build the admin frontend, or run <code>npm run dev</code> during development and open the Vite dev server.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// --- ENDPOINT: SUBIDA Y VALIDACIÓN ---
|
||||
app.post('/upload-cookies', upload.single('cookies'), (req, res) => {
|
||||
@ -201,17 +387,31 @@ app.post('/upload-cookies', upload.single('cookies'), (req, res) => {
|
||||
return res.status(400).json({ status: "error", message: "No se seleccionó archivo." });
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(COOKIES_PATH, 'utf8');
|
||||
const tempPath = req.file.path;
|
||||
const content = fs.readFileSync(tempPath, 'utf8');
|
||||
|
||||
// Validación estricta del formato Netscape
|
||||
if (!content.includes('# Netscape HTTP Cookie File')) {
|
||||
fs.unlinkSync(COOKIES_PATH); // Borramos el inválido
|
||||
fs.unlinkSync(tempPath); // Borramos el inválido
|
||||
return res.status(400).json({
|
||||
status: "error",
|
||||
message: "Formato inválido. Debe ser un archivo de cookies Netscape."
|
||||
});
|
||||
}
|
||||
|
||||
// Move the validated upload into place atomically
|
||||
try {
|
||||
fs.copyFileSync(tempPath, COOKIES_PATH);
|
||||
fs.unlinkSync(tempPath);
|
||||
} catch (mvErr) {
|
||||
// If copy or unlink fails, attempt a rename as fallback
|
||||
try {
|
||||
fs.renameSync(tempPath, COOKIES_PATH);
|
||||
} catch (renameErr) {
|
||||
return res.status(500).json({ status: "error", message: 'Error al guardar cookies: ' + (mvErr.message || renameErr.message) });
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ status: "success", message: "cookies.txt actualizado." });
|
||||
} catch (error) {
|
||||
res.status(500).json({ status: "error", message: error.message });
|
||||
@ -219,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 {
|
||||
@ -234,6 +434,13 @@ app.get('/refresh-cookies', async (req, res) => {
|
||||
});
|
||||
|
||||
// --- INICIO DEL SERVIDOR ---
|
||||
// Servir un favicon simple (1x1 PNG) para evitar 404 en el navegador
|
||||
app.get('/favicon.ico', (req, res) => {
|
||||
const img = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=', 'base64');
|
||||
res.set('Content-Type', 'image/png');
|
||||
res.send(img);
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`
|
||||
🚀 API Enlace Directo News activa
|
||||
|
||||
1151
package-lock.json
generated
1151
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -6,13 +6,16 @@
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"frontend:dev": "vite",
|
||||
"frontend:build": "vite build",
|
||||
"frontend:preview": "vite preview",
|
||||
"clean-cookies": "sed -i 's/^[^#]*#/#/' cookies.txt",
|
||||
"warm": "node -e 'require(\"./cookiewarmer.js\").warmAndExportCookies(\"PzWTM7YvGag\")'",
|
||||
"install-browsers": "npx playwright install chromium",
|
||||
"check-runtime": "/home/xesar/.nvm/versions/node/v20.19.4/bin/node --version",
|
||||
"refresh": "node cookiewarmer.js",
|
||||
"postinstall": "npx playwright install chromium",
|
||||
"open-bot":"/usr/bin/google-chrome --user-data-dir=/home/xesar/bot-profile --no-first-run"
|
||||
"open-bot": "/usr/bin/google-chrome --user-data-dir=/home/xesar/bot-profile --no-first-run"
|
||||
},
|
||||
"keywords": [
|
||||
"youtube-dl",
|
||||
@ -24,12 +27,16 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"playwright": "^1.58.2",
|
||||
"playwright-extra": "^4.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
"autoprefixer": "^10.4.27",
|
||||
"nodemon": "^3.0.1",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
10
vite.config.js
Normal file
10
vite.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
root: path.join(__dirname, 'client'),
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, 'dist'),
|
||||
emptyOutDir: true
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user