674 lines
21 KiB
JavaScript
674 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Script para descargar el contenido del curso de Voley desde
|
|
* Online Education Center usando Playwright.
|
|
*
|
|
* Versión mejorada: navega directamente a cada sección del curso,
|
|
* extrae contenido textual, PDFs, y referencias a videos.
|
|
*
|
|
* Modo de uso:
|
|
* node download_course.mjs
|
|
*
|
|
* Requisitos:
|
|
* - npm install playwright
|
|
* - npx playwright install chromium
|
|
*
|
|
* Para reanudar una descarga interrumpida:
|
|
* Simplemente se vuelve a ejecutar, saltea archivos ya existentes.
|
|
*/
|
|
|
|
import { chromium } from "playwright";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const DOWNLOADS_DIR = path.join(__dirname, "descargas");
|
|
const CREDENTIALS = {
|
|
username: "saldis_karina@hotmail.com",
|
|
password: "Martes13#",
|
|
};
|
|
|
|
// Lista de todas las secciones del curso (Módulo 1 y Módulo 2)
|
|
const SECCIONES = [
|
|
// Módulo 1 (m/19096)
|
|
{
|
|
modulo: "Modulo_1_Planificacion",
|
|
nombre: "Planificacion",
|
|
id: "s/34868",
|
|
seccion: 34868,
|
|
},
|
|
{
|
|
modulo: "Modulo_1_Planificacion",
|
|
nombre: "Preparacion_Atletica",
|
|
id: "s/34869",
|
|
seccion: 34869,
|
|
},
|
|
{
|
|
modulo: "Modulo_1_Planificacion",
|
|
nombre: "Explicacion_TP",
|
|
id: "s/34870",
|
|
seccion: 34870,
|
|
},
|
|
{
|
|
modulo: "Modulo_1_Planificacion",
|
|
nombre: "Seguimientos_Equipos",
|
|
id: "s/35523",
|
|
seccion: 35523,
|
|
},
|
|
{
|
|
modulo: "Modulo_1_Planificacion",
|
|
nombre: "Analisis_Equipos_1",
|
|
id: "s/35524",
|
|
seccion: 35524,
|
|
},
|
|
{
|
|
modulo: "Modulo_1_Planificacion",
|
|
nombre: "Analisis_Equipos_2",
|
|
id: "s/35525",
|
|
seccion: 35525,
|
|
},
|
|
{
|
|
modulo: "Modulo_1_Planificacion",
|
|
nombre: "TP_Final_Unidad",
|
|
id: "s/35526",
|
|
seccion: 35526,
|
|
},
|
|
// Módulo 2 (m/19097)
|
|
{
|
|
modulo: "Modulo_2_Entrenamiento",
|
|
nombre: "Centrales_y_Oponentes",
|
|
id: "s/35519",
|
|
seccion: 35519,
|
|
},
|
|
{
|
|
modulo: "Modulo_2_Entrenamiento",
|
|
nombre: "Puntas_y_Liberos",
|
|
id: "s/35520",
|
|
seccion: 35520,
|
|
},
|
|
{
|
|
modulo: "Modulo_2_Entrenamiento",
|
|
nombre: "Entrenamiento_Armadoras",
|
|
id: "s/35521",
|
|
seccion: 35521,
|
|
},
|
|
{
|
|
modulo: "Modulo_2_Entrenamiento",
|
|
nombre: "Explicacion_TP",
|
|
id: "s/35522",
|
|
seccion: 35522,
|
|
},
|
|
];
|
|
|
|
// IDs de los módulos (necesarios para construir las URLs)
|
|
const MODULO_1 = "m/19096";
|
|
const MODULO_2 = "m/19097";
|
|
|
|
const CAMPUS_BASE =
|
|
"https://voley.onlineeducation.center/es/campus/student/training/e/14197";
|
|
|
|
// ============================================================
|
|
// UTILIDADES
|
|
// ============================================================
|
|
|
|
function ensureDir(dir) {
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
function log(msg) {
|
|
const ts = new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
console.log(`[${ts}] ${msg}`);
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
// ============================================================
|
|
// DESCARGA DE ARCHIVOS
|
|
// ============================================================
|
|
|
|
/**
|
|
* Descarga un archivo desde una URL usando el evento download de Playwright.
|
|
* La URL corresponde a un enlace <a href=".../study-resources/{id}/show?me={me_id}">Abrir Archivo</a>
|
|
* que inicia una descarga nativa del navegador.
|
|
*
|
|
* En lugar de navegar con page.goto() (que carga la respuesta como página),
|
|
* buscamos el enlace por su href y hacemos click para disparar el evento download.
|
|
*/
|
|
async function downloadFile(page, url, filepath) {
|
|
if (fs.existsSync(filepath)) {
|
|
log(` ↺ Ya existe: ${path.basename(filepath)}`);
|
|
return filepath;
|
|
}
|
|
|
|
try {
|
|
// Buscar el enlace en la página por su URL exacta
|
|
const link = page.locator(`a[href="${url}"]`);
|
|
const linkCount = await link.count();
|
|
|
|
if (linkCount === 0) {
|
|
throw new Error(`No se encontró el enlace: ${url}`);
|
|
}
|
|
|
|
// Configurar la espera del evento download antes de hacer click
|
|
const [download] = await Promise.all([
|
|
page.waitForEvent("download", { timeout: 30000 }),
|
|
link.first().click(),
|
|
]);
|
|
|
|
// Guardar el archivo descargado
|
|
await download.saveAs(filepath);
|
|
const suggestedName = download.suggestedFilename();
|
|
const sizeKB = (fs.statSync(filepath).size / 1024).toFixed(1);
|
|
log(
|
|
` ✓ Descargado: ${path.basename(filepath)} (${sizeKB} KB) — original: ${suggestedName || "desconocido"}`,
|
|
);
|
|
return filepath;
|
|
} catch (err) {
|
|
log(` ✗ Error descarga: ${err.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toma screenshot de la página actual.
|
|
*/
|
|
async function screenshot(page, name) {
|
|
const dir = path.join(DOWNLOADS_DIR, "screenshots");
|
|
ensureDir(dir);
|
|
const filepath = path.join(dir, name);
|
|
await page.screenshot({ path: filepath, fullPage: true });
|
|
return filepath;
|
|
}
|
|
|
|
/**
|
|
* Extrae el contenido textual de la página como Markdown simple.
|
|
*/
|
|
async function extractTextAsMarkdown(page, titulo) {
|
|
const text = await page.evaluate(() => {
|
|
// Intentar extraer el contenido principal
|
|
const main = document.querySelector(
|
|
"main, .main-content, .content, #content, .panel-body, .tab-content",
|
|
);
|
|
if (!main) return "";
|
|
|
|
// Clonar para no modificar el DOM real
|
|
const clone = main.cloneNode(true);
|
|
|
|
// Remover elementos no deseados
|
|
clone
|
|
.querySelectorAll(
|
|
"script, style, nav, footer, header, .btn, .dropdown-menu",
|
|
)
|
|
.forEach((el) => el.remove());
|
|
|
|
// Obtener el texto
|
|
return clone.innerText.trim();
|
|
});
|
|
|
|
if (!text) return "";
|
|
|
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
const md = [
|
|
`# ${titulo}`,
|
|
"",
|
|
`> Extraído el ${new Date().toISOString().split("T")[0]}`,
|
|
"",
|
|
...lines.map((l) => l.trim()),
|
|
"",
|
|
].join("\n");
|
|
return md;
|
|
}
|
|
|
|
// ============================================================
|
|
// PROCESAR UNA SECCIÓN DEL CURSO
|
|
// ============================================================
|
|
|
|
async function procesarSeccion(page, seccion) {
|
|
// Determinar qué módulo le corresponde
|
|
const moduloPath = seccion.seccion >= 35519 ? MODULO_2 : MODULO_1;
|
|
|
|
// URL de la página de recursos de esta sección
|
|
const seccionUrl = `${CAMPUS_BASE}/${moduloPath}/${seccion.id}/study-resources/list`;
|
|
|
|
const subdir = path.join("secciones", seccion.modulo, seccion.nombre);
|
|
const dirLocal = path.join(DOWNLOADS_DIR, subdir);
|
|
ensureDir(dirLocal);
|
|
|
|
log(` ── ${seccion.modulo}/${seccion.nombre}`);
|
|
|
|
try {
|
|
await page.goto(seccionUrl, { waitUntil: "networkidle", timeout: 30000 });
|
|
await sleep(2000);
|
|
|
|
// ---- 1. Guardar HTML completo ----
|
|
const htmlContent = await page.content();
|
|
fs.writeFileSync(path.join(dirLocal, "pagina.html"), htmlContent);
|
|
|
|
// ---- 2. Extraer texto como Markdown ----
|
|
const md = await extractTextAsMarkdown(
|
|
page,
|
|
seccion.nombre.replace(/_/g, " "),
|
|
);
|
|
if (md) {
|
|
fs.writeFileSync(path.join(dirLocal, "contenido.md"), md);
|
|
} else {
|
|
// Si no se encontró contenido principal, guardar todo el texto visible
|
|
const fullText = await page.evaluate(() =>
|
|
document.body.innerText.trim(),
|
|
);
|
|
if (fullText) {
|
|
fs.writeFileSync(
|
|
path.join(dirLocal, "contenido.md"),
|
|
`# ${seccion.nombre.replace(/_/g, " ")}\n\n${fullText}\n`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---- 3. Buscar recursos en la página ----
|
|
const recursos = await page.evaluate((seccionUrlLocal) => {
|
|
const items = [];
|
|
|
|
// Buscar enlaces a recursos/archivos del curso
|
|
document.querySelectorAll("a[href]").forEach((el) => {
|
|
const href = el.href;
|
|
// Enlaces a archivos del curso (study-resources/{id}/show)
|
|
if (href.includes("/study-resources/") && href.includes("/show")) {
|
|
items.push({
|
|
tipo: "archivo-curso",
|
|
url: href,
|
|
texto: el.textContent.trim(),
|
|
id: href.match(/study-resources\/(\d+)\/show/)?.[1] || "",
|
|
});
|
|
}
|
|
// PDFs directos
|
|
else if (
|
|
href.match(/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|zip|rar)(\?|$)/i)
|
|
) {
|
|
items.push({
|
|
tipo: "documento",
|
|
url: href,
|
|
texto: el.textContent.trim(),
|
|
});
|
|
}
|
|
// Enlaces de descarga
|
|
else if (
|
|
href.includes("/download/") ||
|
|
href.includes("download=") ||
|
|
el.hasAttribute("download")
|
|
) {
|
|
items.push({
|
|
tipo: "descarga",
|
|
url: href,
|
|
texto: el.textContent.trim(),
|
|
});
|
|
}
|
|
});
|
|
|
|
// Buscar iframes (videos Vimeo/YouTube embebidos)
|
|
document.querySelectorAll("iframe[src]").forEach((el) => {
|
|
const src = el.src;
|
|
if (
|
|
src.includes("vimeo.com") ||
|
|
src.includes("youtube.com") ||
|
|
src.includes("youtu.be")
|
|
) {
|
|
const vimeoMatch = src.match(/vimeo\.com\/video\/(\d+)/);
|
|
const youtubeMatch = src.match(
|
|
/(?:youtube\.com\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]+)/,
|
|
);
|
|
items.push({
|
|
tipo: "video-embed",
|
|
plataforma: src.includes("vimeo") ? "Vimeo" : "YouTube",
|
|
url: src,
|
|
id: vimeoMatch?.[1] || youtubeMatch?.[1] || "",
|
|
titulo: el.title || "",
|
|
});
|
|
}
|
|
// Otros iframes (posiblemente contenido útil)
|
|
else {
|
|
items.push({
|
|
tipo: "iframe",
|
|
url: src,
|
|
titulo: el.title || "",
|
|
});
|
|
}
|
|
});
|
|
|
|
// Buscar videos directos
|
|
document
|
|
.querySelectorAll("video[src], video source[src]")
|
|
.forEach((el) => {
|
|
items.push({
|
|
tipo: "video-directo",
|
|
url: el.src || el.getAttribute("src") || "",
|
|
});
|
|
});
|
|
|
|
// Buscar imágenes relevantes (saltar iconos pequeños)
|
|
document.querySelectorAll("img[src]").forEach((el) => {
|
|
const src = el.src;
|
|
if (
|
|
src &&
|
|
!src.includes("data:") &&
|
|
!src.includes("user-default") &&
|
|
!src.includes("logo")
|
|
) {
|
|
items.push({
|
|
tipo: "imagen",
|
|
url: src,
|
|
alt: el.alt || "",
|
|
});
|
|
}
|
|
});
|
|
|
|
// Buscar contenido en etiquetas <embed> o <object>
|
|
document.querySelectorAll("embed[src], object[data]").forEach((el) => {
|
|
const src = el.src || el.getAttribute("data") || "";
|
|
if (src) {
|
|
items.push({
|
|
tipo: "embed",
|
|
url: src,
|
|
});
|
|
}
|
|
});
|
|
|
|
return items;
|
|
});
|
|
|
|
// ---- 4. Guardar JSON de recursos encontrados ----
|
|
if (recursos.length > 0) {
|
|
fs.writeFileSync(
|
|
path.join(dirLocal, "recursos.json"),
|
|
JSON.stringify(recursos, null, 2),
|
|
);
|
|
}
|
|
|
|
log(
|
|
` Recursos: ${recursos.length} (${recursos.filter((r) => r.tipo === "archivo-curso").length} archivos, ${recursos.filter((r) => r.tipo === "video-embed").length} videos)`,
|
|
);
|
|
|
|
// ---- 5. Descargar archivos del curso (PDFs, docs, etc.) ----
|
|
// Distinguir entre "Abrir Archivo" (descarga real de archivo) y
|
|
// "Abrir Enlace" (página HTML que ya capturamos como pagina.html).
|
|
const archivosCurso = recursos.filter((r) => r.tipo === "archivo-curso");
|
|
for (let i = 0; i < archivosCurso.length; i++) {
|
|
const archivo = archivosCurso[i];
|
|
const texto = archivo.texto.toLowerCase();
|
|
|
|
// Saltar "Abrir Enlace": ya se capturó como HTML en pagina.html
|
|
if (texto.includes("abrir enlace")) {
|
|
log(
|
|
` ↺ Saltando "${archivo.texto}" (${archivo.id}): ya capturado como HTML`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Solo procesar "Abrir Archivo" (descarga real)
|
|
if (!texto.includes("abrir archivo")) {
|
|
log(
|
|
` ↺ Saltando "${archivo.texto}" (${archivo.id}): tipo de recurso no manejado`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const ext = ".bin"; // placeholder, lo detectaremos del contenido
|
|
const filename = `archivo_${archivo.id}_${archivo.texto.replace(/[^a-zA-Z0-9_-]/g, "_").substring(0, 30) || "documento"}${ext}`;
|
|
const filepath = path.join(dirLocal, filename);
|
|
|
|
log(
|
|
` Descargando archivo ${i + 1}/${archivosCurso.length}: "${archivo.texto}" (${archivo.id})...`,
|
|
);
|
|
const result = await downloadFile(page, archivo.url, filepath);
|
|
|
|
// Clasificar el archivo por su contenido (magic bytes) y renombrar
|
|
if (result) {
|
|
const stat = fs.statSync(result);
|
|
if (stat.size > 0) {
|
|
const buffer = fs.readFileSync(result);
|
|
let newPath = null;
|
|
|
|
if (
|
|
buffer[0] === 0x25 &&
|
|
buffer[1] === 0x50 &&
|
|
buffer[2] === 0x44 &&
|
|
buffer[3] === 0x46
|
|
) {
|
|
// Es PDF
|
|
newPath = result.replace(/\.bin$/, ".pdf");
|
|
fs.renameSync(result, newPath);
|
|
log(` ✓ Identificado como PDF: ${path.basename(newPath)}`);
|
|
} else if (buffer[0] === 0x50 && buffer[1] === 0x4b) {
|
|
// Es ZIP (posiblemente .docx, .xlsx, .pptx)
|
|
newPath = result.replace(/\.bin$/, ".zip");
|
|
fs.renameSync(result, newPath);
|
|
log(
|
|
` ⚠ Identificado como ZIP (Office doc?): ${path.basename(newPath)}`,
|
|
);
|
|
} else if (
|
|
buffer[0] === 0x3c || // '<' posible HTML
|
|
(buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) || // BOM UTF-8
|
|
buffer.toString("utf8", 0, 100).includes("<!DOCTYPE") ||
|
|
buffer.toString("utf8", 0, 100).includes("<html")
|
|
) {
|
|
// Es HTML (enlace que se abrió como página en lugar de descarga)
|
|
newPath = result.replace(/\.bin$/, ".html");
|
|
fs.renameSync(result, newPath);
|
|
log(
|
|
` ⚠ Era HTML (posible "Abrir Enlace"): ${path.basename(newPath)}`,
|
|
);
|
|
} else {
|
|
// Formato desconocido, mantener .bin
|
|
log(` ⚠ Formato desconocido: ${path.basename(result)}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- 6. Guardar referencias a videos ----
|
|
const videos = recursos.filter((r) => r.tipo === "video-embed");
|
|
if (videos.length > 0) {
|
|
let videoMd = `# Videos - ${seccion.nombre.replace(/_/g, " ")}\n\n`;
|
|
videoMd += `> Sección: ${seccionUrl}\n\n`;
|
|
for (const v of videos) {
|
|
videoMd += `## ${v.titulo || `Video ${v.plataforma} ID: ${v.id}`}\n\n`;
|
|
videoMd += `- **Plataforma**: ${v.plataforma}\n`;
|
|
videoMd += `- **URL**: ${v.url}\n`;
|
|
videoMd += `- **ID**: ${v.id}\n\n`;
|
|
if (v.plataforma === "Vimeo") {
|
|
videoMd += `Para descargar: yt-dlp "https://player.vimeo.com/video/${v.id}"\n\n`;
|
|
}
|
|
}
|
|
fs.writeFileSync(path.join(dirLocal, "videos.md"), videoMd);
|
|
}
|
|
|
|
// ---- 7. Tomar screenshot de referencia ----
|
|
await screenshot(page, `${seccion.modulo}_${seccion.nombre}.png`);
|
|
|
|
log(` ✓ Completado: ${seccion.nombre}`);
|
|
|
|
return { ok: true, recursos: recursos.length };
|
|
} catch (err) {
|
|
log(` ✗ Error en sección ${seccion.nombre}: ${err.message}`);
|
|
try {
|
|
await screenshot(page, `ERROR_${seccion.modulo}_${seccion.nombre}.png`);
|
|
} catch (_) {}
|
|
return { ok: false, error: err.message };
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// FUNCIÓN PRINCIPAL
|
|
// ============================================================
|
|
|
|
async function main() {
|
|
log("═══════════════════════════════════════════════════════");
|
|
log(" DESCARGA DEL CURSO DE VOLEY - Online Education Center");
|
|
log("═══════════════════════════════════════════════════════");
|
|
log("");
|
|
|
|
// 1. Login
|
|
log("Paso 1: Login en la plataforma...");
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
});
|
|
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1280, height: 900 },
|
|
locale: "es-AR",
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
|
|
const sessionFile = path.join(__dirname, "session.json");
|
|
let loggedIn = false;
|
|
let activeContext = context;
|
|
let activePage = page;
|
|
|
|
try {
|
|
if (fs.existsSync(sessionFile)) {
|
|
log(" Cargando sesión guardada...");
|
|
try {
|
|
const sessionContext = await browser.newContext({
|
|
storageState: sessionFile,
|
|
});
|
|
const sessionPage = await sessionContext.newPage();
|
|
await sessionPage.goto(CAMPUS_BASE, {
|
|
waitUntil: "networkidle",
|
|
timeout: 20000,
|
|
});
|
|
const url = sessionPage.url();
|
|
if (url.includes("/campus/")) {
|
|
loggedIn = true;
|
|
log(" ✓ Sesión recuperada exitosamente");
|
|
// Cerrar page/context original, usar los nuevos
|
|
await activePage.close();
|
|
await activeContext.close();
|
|
activePage = sessionPage;
|
|
activeContext = sessionContext;
|
|
} else {
|
|
await sessionPage.close();
|
|
await sessionContext.close();
|
|
}
|
|
} catch (err) {
|
|
log(` ⚠ No se pudo reusar sesión: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
if (!loggedIn) {
|
|
log(" Abriendo página de login...");
|
|
await page.goto(
|
|
"https://voley.onlineeducation.center/es/campus/student/training/e/14197/m/19097/s/35519/study-resources/list",
|
|
{ waitUntil: "networkidle", timeout: 30000 },
|
|
);
|
|
|
|
await page.waitForSelector("#login-email", { timeout: 15000 });
|
|
await page.fill("#login-email", CREDENTIALS.username);
|
|
await page.fill("#login-password", CREDENTIALS.password);
|
|
await page.click("#btn-login");
|
|
|
|
await page.waitForURL("**/campus/**", { timeout: 30000 });
|
|
log(" ✓ Login exitoso");
|
|
|
|
// Guardar sesión para futuras ejecuciones
|
|
await activeContext.storageState({ path: sessionFile });
|
|
log(" ✓ Sesión guardada en session.json");
|
|
}
|
|
|
|
// 2. Navegar cada sección del curso
|
|
log("");
|
|
log("Paso 2: Descargando contenido de cada sección...");
|
|
log(`Total de secciones: ${SECCIONES.length}`);
|
|
log("");
|
|
|
|
const resultados = [];
|
|
for (let i = 0; i < SECCIONES.length; i++) {
|
|
log(`[${i + 1}/${SECCIONES.length}] Procesando sección...`);
|
|
const resultado = await procesarSeccion(activePage, SECCIONES[i]);
|
|
resultados.push({
|
|
seccion: SECCIONES[i].nombre,
|
|
modulo: SECCIONES[i].modulo,
|
|
...resultado,
|
|
});
|
|
log("");
|
|
}
|
|
|
|
// 3. Resumen final
|
|
log("═══════════════════════════════════════════════════════");
|
|
log(" RESUMEN DE DESCARGA");
|
|
log("═══════════════════════════════════════════════════════");
|
|
log("");
|
|
log(
|
|
` Secciones procesadas: ${resultados.filter((r) => r.ok).length}/${resultados.length}`,
|
|
);
|
|
log(` Secciones con error: ${resultados.filter((r) => !r.ok).length}`);
|
|
log("");
|
|
|
|
for (const r of resultados) {
|
|
const icono = r.ok ? "✓" : "✗";
|
|
const detalles = r.ok ? ` (${r.recursos} recursos)` : ` (${r.error})`;
|
|
log(` ${icono} ${r.modulo}/${r.seccion}${detalles}`);
|
|
}
|
|
|
|
log("");
|
|
log(" Directorio de descarga:");
|
|
log(` ${DOWNLOADS_DIR}`);
|
|
log("");
|
|
log(" Estructura creada:");
|
|
log(" secciones/");
|
|
log(" Modulo_1_Planificacion/");
|
|
for (const sec of SECCIONES.filter(
|
|
(s) => s.modulo === "Modulo_1_Planificacion",
|
|
)) {
|
|
log(
|
|
` ${sec.nombre}/ → pagina.html, contenido.md, recursos.json, videos.md, screenshots`,
|
|
);
|
|
}
|
|
log(" Modulo_2_Entrenamiento/");
|
|
for (const sec of SECCIONES.filter(
|
|
(s) => s.modulo === "Modulo_2_Entrenamiento",
|
|
)) {
|
|
log(
|
|
` ${sec.nombre}/ → pagina.html, contenido.md, recursos.json, videos.md, screenshots`,
|
|
);
|
|
}
|
|
log("");
|
|
log(" Para descargar videos de Vimeo:");
|
|
log(
|
|
' yt-dlp --cookies session.json "https://player.vimeo.com/video/{ID}"',
|
|
);
|
|
log("");
|
|
|
|
// Guardar resultados
|
|
const reportePath = path.join(DOWNLOADS_DIR, "resultado_descarga.json");
|
|
fs.writeFileSync(reportePath, JSON.stringify(resultados, null, 2));
|
|
log(` Reporte guardado: resultado_descarga.json`);
|
|
|
|
log("");
|
|
log("═══════════════════════════════════════════════════════");
|
|
} catch (err) {
|
|
log(`\n✗ ERROR GENERAL: ${err.message}`);
|
|
if (err.stack) log(err.stack);
|
|
try {
|
|
await screenshot(activePage, "ERROR_general.png");
|
|
log(" Screenshot guardado");
|
|
} catch (_) {}
|
|
} finally {
|
|
if (activeContext && activeContext !== context) {
|
|
await activePage.close().catch(() => {});
|
|
await activeContext.close().catch(() => {});
|
|
} else {
|
|
await page.close().catch(() => {});
|
|
await context.close().catch(() => {});
|
|
}
|
|
await browser.close();
|
|
log("Navegador cerrado.");
|
|
}
|
|
}
|
|
|
|
main();
|