#!/bin/bash # # DESCARGAR VIDEOS DEL CURSO DE VOLEY # ==================================== # Descarga los 42 videos de Vimeo con reanudación automática. # Puede detenerse y reanudarse cuando quieras. # # Modo de uso: # bash descargar_videos.sh # # Opciones: # bash descargar_videos.sh --resume # Reanuda descargas pendientes # bash descargar_videos.sh --status # Ver estado de descargas # bash descargar_videos.sh --clean # Borrar todo y empezar de nuevo # set -e # ─── CONFIGURACIÓN ─────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SESSION_JSON="$SCRIPT_DIR/session.json" COOKIES_FILE="$SCRIPT_DIR/.cookies_voley.txt" VIDEOS_DIR="$SCRIPT_DIR/descargas/videos_offline" REGISTRO="$SCRIPT_DIR/.videos_descargados.txt" LISTA_VIDEOS="$SCRIPT_DIR/.lista_videos.txt" DEST_DIR="$SCRIPT_DIR/descargas" LIMIT_RATE="" # Ej: "5M" para limitar a 5 MB/s, vacío = sin límite # ─── COLORES ───────────────────────────────────────────────── VERDE='\033[0;32m' AMARILLO='\033[1;33m' ROJO='\033[0;31m' AZUL='\033[0;34m' RESET='\033[0m' ok() { echo -e " ${VERDE}✓${RESET} $1"; } warn() { echo -e " ${AMARILLO}⚠${RESET} $1"; } err() { echo -e " ${ROJO}✗${RESET} $1"; } info() { echo -e " ${AZUL}→${RESET} $1"; } # ─── HELPERS ───────────────────────────────────────────────── timestamp() { date '+%Y-%m-%d %H:%M:%S' } log() { echo "[$(timestamp)] $1" } check_dependencies() { if ! command -v yt-dlp &>/dev/null; then err "yt-dlp no está instalado." echo " Instalálo con: pip install yt-dlp" exit 1 fi if ! command -v python3 &>/dev/null; then err "python3 no está instalado." exit 1 fi } # ─── CONVERTIR COOKIES ────────────────────────────────────── convertir_cookies() { if [ ! -f "$SESSION_JSON" ]; then err "No se encontró $SESSION_JSON" echo " Ejecutá primero: node download_course.mjs" exit 1 fi log "Convirtiendo cookies de Playwright a formato Netscape..." python3 -c " import json with open('$SESSION_JSON') as f: data = json.load(f) cookies = data.get('cookies', []) lines = ['# Netscape HTTP Cookie File'] for c in cookies: domain = c.get('domain', '') if not domain: continue flag = 'TRUE' if domain.startswith('.') else 'FALSE' path = c.get('path', '/') secure = 'TRUE' if c.get('secure', False) else 'FALSE' exp = c.get('expires', 0) if exp <= 0: exp = 1778594896 # fecha lejana name = c.get('name', '') value = c.get('value', '') lines.append(f'{domain}\t{flag}\t{path}\t{secure}\t{int(exp)}\t{name}\t{value}') with open('$COOKIES_FILE', 'w') as f: f.write('\n'.join(lines) + '\n') print(f'{len(cookies)}') " 2>/dev/null if [ -f "$COOKIES_FILE" ]; then local count=$(wc -l < "$COOKIES_FILE") ok "Cookies convertidas ($((count - 1)))" else err "Error al convertir cookies" exit 1 fi } # ─── EXTRAER LISTA DE VIDEOS ───────────────────────────────── extraer_lista_videos() { log "Extrayendo lista de videos del HTML descargado..." python3 -c " import os, re, json base = '$DEST_DIR/secciones' modulos = ['Modulo_1_Planificacion', 'Modulo_2_Entrenamiento'] # Mapeo de carpetas a IDs de sección mapeo_secciones = { 'Planificacion': ('m/19096', 's/34868'), 'Preparacion_Atletica': ('m/19096', 's/34869'), 'Explicacion_TP': ('m/19096', 's/34870'), 'Seguimientos_Equipos': ('m/19096', 's/35523'), 'Analisis_Equipos_1': ('m/19096', 's/35524'), 'Analisis_Equipos_2': ('m/19096', 's/35525'), 'TP_Final_Unidad': ('m/19096', 's/35526'), 'Centrales_y_Oponentes': ('m/19097', 's/35519'), 'Puntas_y_Liberos': ('m/19097', 's/35520'), 'Entrenamiento_Armadoras': ('m/19097', 's/35521'), 'Explicacion_TP': ('m/19097', 's/35522'), } # Nombre humano de cada sección nombres = { 'Planificacion': 'Planificacion', 'Preparacion_Atletica': 'Preparacion_Atletica', 'Explicacion_TP': 'Explicacion_TP', 'Seguimientos_Equipos': 'Seguimientos_Equipos', 'Analisis_Equipos_1': 'Analisis_Equipos_1', 'Analisis_Equipos_2': 'Analisis_Equipos_2', 'TP_Final_Unidad': 'TP_Final_Unidad', 'Centrales_y_Oponentes': 'Centrales_y_Oponentes', 'Puntas_y_Liberos': 'Puntas_y_Liberos', 'Entrenamiento_Armadoras': 'Entrenamiento_Armadoras', } videos = [] vistos = set() for mod in modulos: mod_path = os.path.join(base, mod) if not os.path.isdir(mod_path): continue for sec in os.listdir(mod_path): html_path = os.path.join(mod_path, sec, 'pagina.html') if not os.path.isfile(html_path): continue html = open(html_path, 'r', errors='ignore').read() if '500 Internal Server Error' in html: continue for m in re.finditer(r'player\.vimeo\.com/video/(\d+)', html): vid = m.group(1) if vid in vistos: continue vistos.add(vid) # Buscar título del tema idx = m.start() before = html[max(0, idx - 400):idx] tm = re.search(r'([^<]+)', before) titulo = tm.group(1).strip() if tm else f'video_{vid}' # Limpiar caracteres no válidos para文件名 titulo = re.sub(r'[<>:\"/\\\\|?*]+', '_', titulo)[:80] # Construir URL de la página embebedora mapa = mapeo_secciones.get(sec) if mapa: mod_id, sec_id = mapa pagina_url = f'https://voley.onlineeducation.center/es/campus/student/training/e/14197/{mod_id}/{sec_id}/study-resources/list' else: pagina_url = '' videos.append({ 'id': vid, 'titulo': titulo, 'seccion': nombres.get(sec, sec), 'modulo': 'M1' if '1' in mod else 'M2', 'url': pagina_url, }) with open('$LISTA_VIDEOS', 'w') as f: json.dump(videos, f, ensure_ascii=False, indent=2) print(len(videos)) " 2>/dev/null local total=$(wc -l < "$LISTA_VIDEOS" 2>/dev/null || echo 0) if [ "$total" -gt 0 ] 2>/dev/null; then ok "Lista de videos generada" python3 -c "import json; print(f' Total: {len(json.load(open(\"$LISTA_VIDEOS\")))} videos')" 2>/dev/null else err "No se encontraron videos en el HTML descargado" exit 1 fi } # ─── DESCARGAR VIDEOS ─────────────────────────────────────── descargar_videos() { mkdir -p "$VIDEOS_DIR" touch "$REGISTRO" # Leer videos ya descargados local descargados=() while IFS= read -r line; do descargados+=("$line") done < "$REGISTRO" # Obtener lista completa local total=$(python3 -c "import json; print(len(json.load(open('$LISTA_VIDEOS'))))" 2>/dev/null || echo 0) if [ "$total" -eq 0 ]; then err "Lista de videos vacía" exit 1 fi echo "" echo " Total: $total videos" echo " Ya descargados: ${#descargados[@]}" echo " Pendientes: $((total - ${#descargados[@]}))" echo "" # Procesar cada video for i in $(seq 0 $((total - 1))); do local data=$(python3 -c " import json with open('$LISTA_VIDEOS') as f: v = json.load(f) v = v[$i] print(f'{v[\"id\"]}|{v[\"titulo\"]}|{v[\"seccion\"]}|{v[\"modulo\"]}|{v[\"url\"]}')" 2>/dev/null) local id=$(echo "$data" | cut -d'|' -f1) local titulo=$(echo "$data" | cut -d'|' -f2) local seccion=$(echo "$data" | cut -d'|' -f3) local modulo=$(echo "$data" | cut -d'|' -f4) local pagina_url=$(echo "$data" | cut -d'|' -f5-) # Verificar si ya se descargó if grep -q "^$id|" "$REGISTRO" 2>/dev/null; then continue fi # Nombre del archivo de salida local safe_name="${modulo}_${seccion}_${titulo}" safe_name=$(echo "$safe_name" | sed 's/[^a-zA-Z0-9_]/_/g' | cut -c1-120) local output_path="$VIDEOS_DIR/${safe_name}.mp4" # Si el archivo ya existe en disco pero no en el registro, agregarlo if [ -f "$output_path" ]; then echo "${id}|${safe_name}.mp4" >> "$REGISTRO" ok "[$((i+1))/$total] Ya existe: ${titulo:0:50}" continue fi echo "" echo " ─────────────────────────────────────────────" printf " [%d/%d] 🎬 %s\n" $((i+1)) "$total" "${titulo:0:70}" echo " ─────────────────────────────────────────────" echo "" if [ -z "$pagina_url" ]; then warn "Sin URL de página embebedora, intentando con URL directa..." pagina_url="https://player.vimeo.com/video/${id}" fi # Construir comando yt-dlp local cmd="yt-dlp" cmd+=" --cookies \"$COOKIES_FILE\"" cmd+=" -o \"$output_path\"" cmd+=" --merge-output-format mp4" cmd+=" --no-playlist" cmd+=" --no-progress" cmd+=" --console-title" cmd+=" --retries 3" cmd+=" --continue" if [ -n "$LIMIT_RATE" ]; then cmd+=" --limit-rate $LIMIT_RATE" fi cmd+=" \"$pagina_url\"" # Ejecutar descarga if eval "$cmd" 2>&1; then if [ -f "$output_path" ]; then local size=$(du -h "$output_path" | cut -f1) echo "${id}|${safe_name}.mp4" >> "$REGISTRO" ok "Descargado: ${titulo:0:40} ($size)" else # Buscar archivo con otro nombre local found=$(find "$VIDEOS_DIR" -name "*${id}*" -o -name "*${titulo:0:20}*" 2>/dev/null | head -1) if [ -n "$found" ]; then local fname=$(basename "$found") echo "${id}|${fname}" >> "$REGISTRO" local size=$(du -h "$found" | cut -f1) ok "Descargado (con otro nombre): ${titulo:0:40} ($size)" else warn "Puede que se haya descargado pero no se encontró el archivo esperado." warn "Ruta buscada: $output_path" fi fi else err "Error descargando: ${titulo:0:50}" fi # Pequeña pausa entre descargas para no saturar sleep 2 done echo "" local pendientes=$((total - $(wc -l < "$REGISTRO" 2>/dev/null || echo 0))) if [ "$pendientes" -le 0 ]; then echo " 🎉 ¡Todos los videos descargados!" else echo " Quedan $pendientes videos pendientes." echo " Para reanudar: bash $0" fi } # ─── GENERAR LIBRO CON VIDEOS ─────────────────────────────── generar_libro() { log "Generando libro de estudio con videos locales..." node -e " const fs = require('fs'); const path = require('path'); // Leer registro de videos descargados const registro = '$REGISTRO'; const videosMap = {}; if (fs.existsSync(registro)) { const lines = fs.readFileSync(registro, 'utf-8').trim().split('\n'); for (const line of lines) { const [id, archivo] = line.split('|'); if (id && archivo) videosMap[id] = archivo; } } console.log(' Videos mapeados:', Object.keys(videosMap).length); // Guardar mapa para que lo use el script principal const mapaPath = '$SCRIPT_DIR/.mapa_videos.json'; fs.writeFileSync(mapaPath, JSON.stringify(videosMap, null, 2)); " 2>/dev/null # Ahora regenerar el libro usando el script existente if [ -f "$SCRIPT_DIR/generar_dashboard.mjs" ]; then node "$SCRIPT_DIR/generar_dashboard.mjs" 2>&1 else warn "No se encontró generar_dashboard.mjs" warn "El libro se generará sin videos la próxima vez que ejecutes node generar_dashboard.mjs" fi } # ─── ESTADO ────────────────────────────────────────────────── mostrar_estado() { echo "" echo " ─── ESTADO DE DESCARGA ───" echo "" if [ ! -f "$LISTA_VIDEOS" ]; then echo " Lista de videos no generada." echo " Ejecutá primero: bash $0" return fi local total=$(python3 -c "import json; print(len(json.load(open('$LISTA_VIDEOS'))))" 2>/dev/null || echo 0) local descargados=$(wc -l < "$REGISTRO" 2>/dev/null || echo 0) local pendientes=$((total - descargados)) echo " Total videos: $total" echo " Descargados: $descargados" echo " Pendientes: $pendientes" echo "" if [ "$descargados" -gt 0 ] && [ -d "$VIDEOS_DIR" ]; then local total_size=$(du -sh "$VIDEOS_DIR" 2>/dev/null | cut -f1) echo " Espacio usado: $total_size" echo "" echo " Últimos descargados:" tail -5 "$REGISTRO" 2>/dev/null | while IFS='|' read -r id archivo; do if [ -n "$archivo" ] && [ -f "$VIDEOS_DIR/$archivo" ]; then local size=$(du -h "$VIDEOS_DIR/$archivo" 2>/dev/null | cut -f1) echo " ✓ $archivo ($size)" fi done fi echo "" } # ─── LIMPIAR ───────────────────────────────────────────────── limpiar() { echo "" warn "¿Borrar todos los videos descargados? (s/N): " read -r respuesta if [ "$respuesta" = "s" ] || [ "$respuesta" = "S" ]; then rm -rf "$VIDEOS_DIR" "$REGISTRO" "$LISTA_VIDEOS" "$COOKIES_FILE" ok "Eliminado. Listo para empezar de nuevo." else ok "Operación cancelada." fi } # ─── MAIN ──────────────────────────────────────────────────── main() { echo "" echo "═══════════════════════════════════════════════════" echo " 🏐 DESCARGAR VIDEOS — Curso de Vóley" echo "═══════════════════════════════════════════════════" echo "" check_dependencies # Procesar argumentos case "${1:-}" in --status|-s) convertir_cookies extraer_lista_videos mostrar_estado exit 0 ;; --clean|-c) limpiar exit 0 ;; --resume|-r) true # simplemente continua ;; *) # Primer ejecución: setup completo if [ ! -f "$COOKIES_FILE" ]; then convertir_cookies fi if [ ! -f "$LISTA_VIDEOS" ]; then extraer_lista_videos fi ;; esac # Asegurar cookies if [ ! -f "$COOKIES_FILE" ]; then convertir_cookies fi if [ ! -f "$LISTA_VIDEOS" ]; then extraer_lista_videos fi descargar_videos echo "" echo "═══════════════════════════════════════════════════" echo " Listo." echo "═══════════════════════════════════════════════════" echo "" echo " Videos en: $VIDEOS_DIR" echo " Para retomar: bash $0 --resume" echo " Para ver estado: bash $0 --status" echo "" } main "$@"