initial commit
This commit is contained in:
commit
5e55dbadd7
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
||||
28
cookies.txt
Normal file
28
cookies.txt
Normal file
@ -0,0 +1,28 @@
|
||||
# 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
|
||||
77
cookiewarmer.js
Normal file
77
cookiewarmer.js
Normal file
@ -0,0 +1,77 @@
|
||||
const { chromium } = require('playwright-extra');
|
||||
const stealth = require('puppeteer-extra-plugin-stealth')();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
chromium.use(stealth);
|
||||
|
||||
async function exportCookiesFromBotProfile() {
|
||||
const userDataDir = '/home/xesar/bot-profile';
|
||||
const outputPath = path.resolve(__dirname, 'cookies.txt');
|
||||
const lockFile = path.join(userDataDir, 'SingletonLock');
|
||||
|
||||
// 1. Limpieza de procesos previos para evitar el error de "Profile in use"
|
||||
console.log('🧹 Limpiando bloqueos de Chrome...');
|
||||
try {
|
||||
execSync(`rm -f "${lockFile}"`);
|
||||
execSync('pkill -f "user-data-dir=/home/xesar/bot-profile" || true');
|
||||
} catch (e) {}
|
||||
|
||||
console.log('🚀 Iniciando Chrome con perfil de Bot...');
|
||||
|
||||
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
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-blink-features=AutomationControlled'
|
||||
]
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
// 2. Navegar a YouTube para que el navegador cargue las cookies en memoria activa
|
||||
console.log('📺 Refrescando sesión en YouTube...');
|
||||
await page.goto('https://www.youtube.com', { waitUntil: 'networkidle', timeout: 60000 });
|
||||
|
||||
// Esperamos un momento para que se carguen las cookies Secure-3PSID y similares
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 3. Obtener cookies (Playwright las extrae YA DESCIFRADAS del navegador)
|
||||
const cookies = await context.cookies();
|
||||
|
||||
// 4. Formatear al estándar Netscape (el que usa tu extensión y yt-dlp)
|
||||
let netscape = "# Netscape HTTP Cookie File\n# This file is generated by Enlace Directo News Bot\n\n";
|
||||
|
||||
cookies.forEach(c => {
|
||||
const domain = c.domain.startsWith('.') ? c.domain : `.${c.domain}`;
|
||||
const hostOnly = "TRUE";
|
||||
const path = c.path;
|
||||
const secure = c.secure ? "TRUE" : "FALSE";
|
||||
const expires = c.expires ? Math.round(c.expires) : 0;
|
||||
|
||||
// Estructura: domain \t flag \t path \t secure \t expiration \t name \t value
|
||||
netscape += `${domain}\t${hostOnly}\t${path}\t${secure}\t${expires}\t${c.name}\t${c.value}\n`;
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputPath, netscape);
|
||||
|
||||
console.log(`✅ ¡Éxito! Se exportaron ${cookies.length} cookies.`);
|
||||
console.log(`📄 Archivo guardado en: ${outputPath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error durante la exportación:', error.message);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Ejecución automática si se llama al script directamente
|
||||
if (require.main === module) {
|
||||
exportCookiesFromBotProfile();
|
||||
}
|
||||
|
||||
module.exports = { exportCookiesFromBotProfile };
|
||||
244
index.js
Normal file
244
index.js
Normal file
@ -0,0 +1,244 @@
|
||||
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
|
||||
------------------------------------------
|
||||
`);
|
||||
});
|
||||
2021
package-lock.json
generated
Normal file
2021
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "enlace-directo-api",
|
||||
"version": "1.0.0",
|
||||
"description": "API de extracción de streaming y transcripciones para Enlace Directo News",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"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"
|
||||
},
|
||||
"keywords": [
|
||||
"youtube-dl",
|
||||
"streaming",
|
||||
"m3u8",
|
||||
"transcription"
|
||||
],
|
||||
"author": "Erik / Xesar",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"playwright": "^1.58.2",
|
||||
"playwright-extra": "^4.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user