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
This commit is contained in:
@@ -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
|
||||||
@@ -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 [email protected]"
|
||||||
|
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 ""
|
||||||
@@ -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 [email protected]"
|
||||||
|
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
|
||||||
@@ -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 ""
|
||||||
Reference in New Issue
Block a user