#!/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 Abrir Archivo
* 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