483 lines
16 KiB
Bash
483 lines
16 KiB
Bash
#!/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'<em>([^<]+)</em>', 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 "$@"
|