Fix: www/ movido a raíz, estructura final

This commit is contained in:
Ricardo Monla
2026-05-04 13:52:29 -03:00
parent 1e13888679
commit 17ff243bfb
337 changed files with 46456 additions and 19 deletions
+673
View File
@@ -0,0 +1,673 @@
#!/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();