From 3c49f73fa2b2fe14318d504e840a8810c517fe5e Mon Sep 17 00:00:00 2001 From: urieljareth Date: Thu, 10 Sep 2026 00:32:32 -0600 Subject: [PATCH] feat: arranque y bootstrap multiplataforma (macOS/Linux) - scripts/bootstrap.sh: setup idempotente (python>=3.10, venv, extras, ffmpeg/node con hints de brew/apt, config.yaml desde example) - scripts/start-server.sh / stop-server.sh: equivalentes bash de los .ps1 (reuso de servidor via .run/server.info, puerto libre 8000-8100, healthz, apertura de navegador con open/xdg-open) - Makefile: setup/serve/stop/test/clean (Windows apunta a los .bat/.ps1) - compatible con bash 3.2 de macOS, sin bashismos --- Makefile | 44 ++++++++++ scripts/bootstrap.sh | 115 +++++++++++++++++++++++++ scripts/start-server.sh | 182 ++++++++++++++++++++++++++++++++++++++++ scripts/stop-server.sh | 91 ++++++++++++++++++++ 4 files changed, 432 insertions(+) create mode 100644 Makefile create mode 100644 scripts/bootstrap.sh create mode 100644 scripts/start-server.sh create mode 100644 scripts/stop-server.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..81c8a55 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +# yt-scraper — atajos para macOS/Linux (y test/clean multiplataforma). +# +# macOS trae `make` de fabrica, asi que un Mac nuevo queda sirviendo con: +# make setup && make serve +# En Windows estos targets solo orientan: usa los .bat / .ps1 del proyecto +# (start-server.bat, stop-server.bat, scripts/doctor.ps1). + +# En Windows (GNU make define OS=Windows_NT) los scripts .sh no aplican. +ifeq ($(OS),Windows_NT) + +setup: + @echo [Windows] Los scripts .sh son para macOS/Linux. Usa scripts\doctor.ps1 o instala a mano: pip install -e ".[dev,web,analysis]" + +serve: + @echo [Windows] Usa start-server.bat + +stop: + @echo [Windows] Usa stop-server.bat + +test: + uv run python -m pytest -q || python -m pytest -q + +else + +setup: + @bash scripts/bootstrap.sh + +serve: + @bash scripts/start-server.sh + +stop: + @bash scripts/stop-server.sh + +test: + @if command -v uv >/dev/null 2>&1; then uv run python -m pytest -q; else python -m pytest -q; fi + +endif + +# Borra solo caches de Python; no toca datos, venv ni .run/. +clean: + @find . -path ./.venv -prune -o -path ./.venv-312 -prune -o -type d \( -name __pycache__ -o -name .pytest_cache \) -prune -exec rm -rf {} + + @echo caches eliminadas. + +.PHONY: setup serve stop test clean diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..4dc15c4 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# bootstrap.sh — prepara una maquina nueva (macOS/Linux) para yt-scraper. +# Idempotente: seguro de ejecutar tantas veces como haga falta. +# +# bash scripts/bootstrap.sh (o `make setup`) +# +# Que hace: +# 1. Localiza Python >= 3.10 (prefiere 3.12; pista de brew si falta). +# 2. Crea el venv .venv/ si no existe (o lo reusa). +# 3. Instala el paquete con los extras dev, web y analysis. +# 4. Revisa herramientas opcionales: ffmpeg y node (con pistas de instalacion). +# 5. Copia config.example.yaml -> config.yaml si aun no existe. +# +# Compatible con el bash 3.2 de macOS y con POSIX sh. + +set -u + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" || exit 1 +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" || exit 1 +cd "$ROOT" || exit 1 + +say() { printf '%s\n' "$*"; } +ok() { say " [ok] $*"; } +warn() { say " [warn] $*"; } +err() { say " [error] $*"; } + +OS="$(uname)" + +say "" +say " [yt-scraper] preparando el entorno en $ROOT ..." + +# --- (1) Python >= 3.10 (se prefiere 3.12) --- +PY="" +for candidate in python3.12 python3.11 python3.10 python3 python; do + command -v "$candidate" >/dev/null 2>&1 || continue + ver="$("$candidate" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || true)" + case "$ver" in + 3.1[0-9] | 3.[2-9][0-9] | 4.*) + PY="$(command -v "$candidate")" + break + ;; + esac +done + +if [ -z "$PY" ]; then + err "no encontre Python >= 3.10 en el PATH." + if [ "$OS" = "Darwin" ]; then + say " instalalo con Homebrew: brew install python@3.12" + else + say " instalalo con el gestor de tu distro, p.ej.:" + say " sudo apt install python3 python3-venv python3-pip" + fi + exit 1 +fi +ok "Python encontrado: $PY ($("$PY" -c 'import sys; print(sys.version.split()[0])'))" + +# --- (2) venv (crear si falta, reusar si ya existe) --- +if [ ! -x ".venv/bin/python" ]; then + say " [...] creando venv .venv/ ..." + if ! "$PY" -m venv .venv; then + err "no se pudo crear el venv (en Debian/Ubuntu falta: sudo apt install python3-venv)." + exit 1 + fi +else + ok "venv .venv/ ya existe; reusandolo." +fi +VPY=".venv/bin/python" + +# --- (3) dependencias (paquete + extras dev/web/analysis) --- +say " [...] instalando dependencias: pip install -e \"[dev,web,analysis]\" ..." +"$VPY" -m pip install --quiet --upgrade pip || true +if ! "$VPY" -m pip install -e ".[dev,web,analysis]"; then + err "fallo pip install; revisa el mensaje de arriba." + exit 1 +fi +ok "dependencias instaladas." + +# --- (4) herramientas opcionales --- +if command -v ffmpeg >/dev/null 2>&1; then + ok "ffmpeg presente ($(command -v ffmpeg))." +else + warn "ffmpeg no encontrado (solo hace falta para descargar audio MP3)." + if [ "$OS" = "Darwin" ]; then + say " brew install ffmpeg" + else + say " sudo apt install ffmpeg # Debian/Ubuntu (dnf/pacman en otras distros)" + fi +fi + +if command -v node >/dev/null 2>&1; then + ok "node presente ($(command -v node)) — runtime JS para los retos de yt-dlp." +else + warn "node no encontrado (yt-dlp lo usa como runtime JS para algunos desafios)." + if [ "$OS" = "Darwin" ]; then + say " brew install node" + else + say " usa el gestor de tu distro o descargalo de https://nodejs.org" + fi +fi + +# --- (5) configuracion inicial --- +if [ -f "config.example.yaml" ] && [ ! -f "config.yaml" ]; then + cp config.example.yaml config.yaml + ok "config.yaml creado a partir de config.example.yaml (revisa canales y rutas)." +elif [ -f "config.yaml" ]; then + ok "config.yaml ya existe; no se toca." +fi + +# --- siguientes pasos --- +say "" +say " Listo. Siguientes pasos:" +say " - arranca el servidor: bash scripts/start-server.sh (o: make serve)" +say " - detene el servidor: bash scripts/stop-server.sh (o: make stop)" +say " - o usa la CLI: .venv/bin/yt-scraper --help" +say "" diff --git a/scripts/start-server.sh b/scripts/start-server.sh new file mode 100644 index 0000000..51bc32d --- /dev/null +++ b/scripts/start-server.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# start-server.sh — arranque del servidor web de yt-scraper en macOS/Linux. +# Espejo de scripts/start-server.ps1 (Windows); mismo formato de .run/server.info +# ("PUERTO PID FECHA"), asi ambos conviven sobre el mismo proyecto. +# +# Uso: +# bash scripts/start-server.sh (o `make serve`) +# PYTHON_EXE=/ruta/python3 bash scripts/start-server.sh # igual que -PythonExe en el .ps1 +# +# Compatible con el bash 3.2 de macOS y con POSIX sh: sin arrays asociativos, +# sin ${var,,}, sin mapfile. + +set -u + +# --- raiz del proyecto (carpeta padre de scripts/) --- +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" || exit 1 +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" || exit 1 +cd "$ROOT" || exit 1 + +RUN_DIR=".run" +INFO_FILE="$RUN_DIR/server.info" +LOG_FILE="$RUN_DIR/server.log" +ERR_LOG_FILE="$RUN_DIR/server.err.log" +HOST="127.0.0.1" +PORT_MIN=8000 +PORT_MAX=8100 +READY_TIMEOUT=30 # segundos maximos esperando /healthz + +say() { printf '%s\n' "$*"; } + +# --- interprete Python: venv del proyecto si existe, si no python3 --- +if [ -n "${PYTHON_EXE:-}" ]; then + PY="$PYTHON_EXE" +elif [ -x ".venv/bin/python" ]; then + PY=".venv/bin/python" +else + PY="$(command -v python3 || command -v python || true)" + if [ -z "$PY" ]; then + say " [error] no encontre python3. Instalalo con: brew install python@3.12" + exit 1 + fi +fi + +# --- GET a /healthz; exit 0 si responde 'ok' (curl, con fallback a python) --- +healthz_ok() { + # $1 = puerto + if command -v curl >/dev/null 2>&1; then + curl -fsS --max-time 2 "http://$HOST:$1/healthz" 2>/dev/null | grep -q ok + else + "$PY" -c "import urllib.request,sys; sys.exit(0 if b'ok' in urllib.request.urlopen('http://$HOST:$1/healthz', timeout=2).read() else 1)" >/dev/null 2>&1 + fi +} + +# --- abre el navegador segun el SO; si no puede, imprime la URL --- +open_browser() { + case "$(uname)" in + Darwin) + if command -v open >/dev/null 2>&1; then + open "$1" && say " [ok] navegador abierto -> $1" && return 0 + fi + ;; + *) + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$1" && say " [ok] navegador abierto -> $1" && return 0 + fi + ;; + esac + say " [info] abre esta URL en tu navegador: $1" +} + +mkdir -p "$RUN_DIR" + +say "" +say " [yt-scraper] iniciando servidor local..." + +# --- (1) servidor ya registrado y vivo? -> reutilizarlo --- +# El puerto respondiendo es la verdad: si /healthz contesta, ese ES el +# servidor corriendo, aunque el PID registrado haya muerto (el proceso +# padre puede terminar y dejar el hijo vivo). +if [ -f "$INFO_FILE" ]; then + reg_port="" + first_line="$(head -n 1 "$INFO_FILE" 2>/dev/null || true)" + # primera linea: "PUERTO PID FECHA" + set -- $first_line + reg_port="${1:-}" + if [ -n "$reg_port" ] && healthz_ok "$reg_port"; then + say " [ok] servidor ya activo en puerto $reg_port" + say " URL: http://localhost:$reg_port" + say " log: $LOG_FILE" + say " stop: bash scripts/stop-server.sh" + open_browser "http://$HOST:$reg_port" + say "" + exit 0 + fi + say " [info] Habia un registro viejo de otra sesion; limpiandolo..." + rm -f "$INFO_FILE" +fi + +# --- (2) puertos libres en 8000-8100 (prueba de bind con Python) --- +FREE_PORTS="$("$PY" - <<'PYEOF' 2>/dev/null +import socket +free = [] +for port in range(8000, 8101): + s = socket.socket() + try: + s.bind(("127.0.0.1", port)) + except OSError: + s.close() + continue + s.close() + free.append(str(port)) + if len(free) >= 8: + break +print(" ".join(free)) +PYEOF +)" +if [ -z "$FREE_PORTS" ]; then + say " [error] no se encontro ningun puerto libre ($PORT_MIN-$PORT_MAX)" + exit 2 +fi + +# --- (3) lanzar uvicorn en cada candidato hasta que uno responda --- +launched=0 +for PORT in $FREE_PORTS; do + rm -f "$LOG_FILE" "$ERR_LOG_FILE" + say " [...] Encendiendo el servidor (intento $PORT)..." + nohup "$PY" -m uvicorn yt_scraper.webapp.app:app \ + --host "$HOST" --port "$PORT" --log-level info \ + >>"$LOG_FILE" 2>>"$ERR_LOG_FILE" & + SERVER_PID=$! + say " El servidor corre en segundo plano. Registro: $LOG_FILE" + + # Esperar /healthz (max ~30 s); si el proceso muere, probar otro puerto. + waited=0 + ready=0 + while [ "$waited" -lt "$READY_TIMEOUT" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + say " [warn] el proceso murio al arrancar; probando el siguiente puerto..." + break + fi + if healthz_ok "$PORT"; then + ready=1 + break + fi + sleep 1 + waited=$((waited + 1)) + done + + if [ "$ready" -ne 1 ]; then + # Mostrar el motivo probable del fallo para poder diagnosticarlo. + if [ -f "$ERR_LOG_FILE" ]; then + say " (ultimas lineas de $ERR_LOG_FILE):" + tail -n 6 "$ERR_LOG_FILE" 2>/dev/null | sed 's/^/ /' + fi + kill "$SERVER_PID" 2>/dev/null + continue + fi + + printf '%s %s %s\n' "$PORT" "$SERVER_PID" "$(date '+%Y-%m-%dT%H:%M:%S%z')" > "$INFO_FILE" + say " [ok] Servidor encendido." + say " URL: http://localhost:$PORT" + say " log: $LOG_FILE" + say " stop: bash scripts/stop-server.sh" + open_browser "http://$HOST:$PORT" + launched=1 + break +done + +if [ "$launched" -ne 1 ]; then + say " [error] no se pudo iniciar el servidor en ninguno de los puertos candidatos." + if [ -f "$LOG_FILE" ]; then + say " --- ultimo log del servidor ---" + tail -n 25 "$LOG_FILE" 2>/dev/null | sed 's/^/ /' + else + say ' instala dependencias: pip install -e ".[web]"' + fi + say "" + exit 3 +fi + +say "" +exit 0 diff --git a/scripts/stop-server.sh b/scripts/stop-server.sh new file mode 100644 index 0000000..a67b8c4 --- /dev/null +++ b/scripts/stop-server.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# stop-server.sh — detiene el servidor web registrado en .run/server.info. +# Espejo de scripts/stop-server.ps1 (Windows). Mensajes en espanol como el .ps1. +# +# Uso: bash scripts/stop-server.sh (o `make stop`) +# Compatible con el bash 3.2 de macOS y con POSIX sh. + +set -u + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" || exit 1 +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" || exit 1 +cd "$ROOT" || exit 1 + +INFO_FILE=".run/server.info" + +say() { printf '%s\n' "$*"; } + +# --- algo sigue escuchando en el puerto? (nc, con fallback a python3) --- +port_busy() { + # $1 = puerto; exit 0 si hay un proceso aceptando conexiones ahi + if command -v nc >/dev/null 2>&1; then + nc -z 127.0.0.1 "$1" >/dev/null 2>&1 + elif command -v python3 >/dev/null 2>&1; then + python3 - "$1" <<'PYEOF' >/dev/null 2>&1 +import socket, sys +s = socket.socket() +s.settimeout(1) +try: + s.connect(("127.0.0.1", int(sys.argv[1]))) +except OSError: + sys.exit(1) +PYEOF + else + return 1 + fi +} + +say "" + +if [ ! -f "$INFO_FILE" ]; then + say " [yt-scraper] no hay servidor registrado; nada que detener." + say " [ok] nada que detener." + say "" + exit 0 +fi + +first_line="$(head -n 1 "$INFO_FILE" 2>/dev/null || true)" +# primera linea: "PUERTO PID FECHA" +set -- $first_line +PORT="${1:-}" +PIDV="${2:-}" + +say " [yt-scraper] Apagando el servidor..." + +# PID ausente, no-numerico o ya muerto -> ya estaba apagado. +if [ -z "$PIDV" ]; then + say " [info] El servidor ya estaba apagado." +else + case "$PIDV" in + *[!0-9]* | "") + say " [info] registro corrupto; solo limpio el archivo." + ;; + *) + if ! kill -0 "$PIDV" 2>/dev/null; then + say " [info] El servidor ya estaba apagado." + else + # kill amable, ~4 s de gracia y luego kill -9. + kill "$PIDV" 2>/dev/null + i=0 + while [ "$i" -lt 10 ] && kill -0 "$PIDV" 2>/dev/null; do + sleep 0.4 + i=$((i + 1)) + done + if kill -0 "$PIDV" 2>/dev/null; then + say " [kill] no cerro solo; forzando (kill -9)..." + kill -9 "$PIDV" 2>/dev/null + sleep 0.5 + fi + fi + ;; + esac +fi + +rm -f "$INFO_FILE" + +if [ -n "$PORT" ] && port_busy "$PORT"; then + say " [warn] Algo sigue ocupando el puerto $PORT. Revisa con: lsof -i :$PORT" +else + say " [ok] puerto liberado, servidor detenido." +fi +say ""