diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2ee95d --- /dev/null +++ b/README.md @@ -0,0 +1,496 @@ +# KiraStream + +Servidor de gestión de líneas IPTV compatible con el protocolo **Xtream Codes**. Permite administrar proveedores, usuarios, catálogos y monitorización en tiempo real desde un panel web, sirviendo los streams a cualquier reproductor IPTV estándar. + +--- + +## Índice + +- [Características](#características) +- [Arquitectura](#arquitectura) +- [Requisitos](#requisitos) +- [Instalación](#instalación) +- [Configuración](#configuración) +- [Estructura del proyecto](#estructura-del-proyecto) +- [API](#api) +- [Panel de administración](#panel-de-administración) +- [Gestión del servicio](#gestión-del-servicio) +- [Registro y monitorización](#registro-y-monitorización) +- [Notas técnicas de estabilidad](#notas-técnicas-de-estabilidad) + +--- + +## Características + +- **Proxy de streams en tiempo real** — reencaminamiento de canales en directo mediante ffmpeg en modo copy (sin recodificación). Fan-out a múltiples clientes sobre una única conexión upstream. +- **API compatible con Xtream Codes** — funciona con IPTV Smarters, TiviMate, Perfect Player, VLC, Kodi y cualquier reproductor que soporte el protocolo Xtream. +- **Gestión de proveedores** — múltiples cuentas, múltiples dominios/CDN por cuenta con failover automático, sincronización de catálogos y health check cada 5 minutos. +- **Gestión de usuarios** — usuarios independientes con límite de conexiones simultáneas, prioridad, fecha de expiración y catálogos personalizados. +- **EPG integrado** — fusión de múltiples fuentes XMLTV, filtrado por canal y servicio en `/xmltv.php`. +- **VOD y Series** — proxy transparente para películas y series con soporte de `Range` (seek). +- **Integración Jellyfin** — biblioteca local de películas y series desde un servidor Jellyfin. +- **Monitorización en tiempo real** — WebSocket con métricas de streams, clientes, CPU de ffmpeg, salud A/V, buffer y reconexiones. +- **Registro de fallos** — eventos de caída y recuperación de streams persistidos en base de datos y visibles en el panel. +- **Reconexión transparente** — al caer un proveedor el buffer de cola del servidor absorbe el tiempo de reconexión; los clientes no ven pantalla negra. + +--- + +## Arquitectura + +``` +Cliente IPTV (TV/móvil/PC) + │ HTTP TS stream + ▼ + Nginx :80 + │ proxy_pass + ▼ + FastAPI / Uvicorn :8000 + │ + ├── ProviderPool (pool.py) + │ └── BroadcastGroup × canal activo + │ └── ffmpeg (copy mode) ← proveedor IPTV + │ + ├── VodTracker (vod_tracker.py) ← películas / series + ├── EPGManager (epg_manager.py) ← XMLTV fusion + └── HealthChecker ← ping proveedores c/5min + +PostgreSQL 17 ←→ SQLAlchemy async (asyncpg) +Redis ←→ pub/sub monitorización + slots de proveedor +``` + +### Flujo de un canal en directo + +1. El reproductor solicita `GET /{user}/{pass}/{stream_id}`. +2. `ProviderPool.acquire()` busca o crea un `BroadcastGroup` para ese canal. +3. El grupo lanza un proceso ffmpeg que lee del proveedor y escribe MPEG-TS en stdout. +4. Cada chunk (64 KB) se despacha a las colas asyncio de todos los clientes conectados. +5. El `StreamingResponse` de FastAPI lee de la cola y lo envía al reproductor via HTTP chunked. +6. Cuando el proveedor cierra la sesión (~2 min), ffmpeg muere limpiamente y el loop de Python lo reinicia con una conexión fresca. El buffer de cola cubre el tiempo de reconexión. + +--- + +## Requisitos + +| Componente | Versión mínima | +|---|---| +| Python | 3.11+ (probado en 3.13) | +| PostgreSQL | 14+ (probado en 17) | +| Redis | 6+ | +| Nginx | 1.18+ | +| ffmpeg | 5+ (probado en 7.1) | +| Node.js | 18+ (solo para compilar el frontend) | + +--- + +## Instalación + +### 1. Dependencias del sistema + +```bash +apt install -y python3 python3-venv python3-pip \ + postgresql redis-server nginx ffmpeg \ + nodejs npm +``` + +### 2. Clonar el repositorio + +```bash +git clone https://git.kiracloud.es/joaquin/KiraTV.git /opt/kirastream +cd /opt/kirastream +``` + +### 3. Entorno Python + +```bash +python3 -m venv venv +source venv/bin/activate +pip install fastapi uvicorn[standard] sqlalchemy[asyncio] asyncpg \ + aiohttp redis pydantic-settings alembic python-jose \ + passlib python-multipart +``` + +### 4. Base de datos + +```bash +# Crear usuario y base de datos en PostgreSQL +su - postgres -c "createuser kirastream" +su - postgres -c "createdb -O kirastream kirastream" +su - postgres -c "psql -c \"ALTER USER kirastream PASSWORD 'kirastream_secret_2024';\"" +``` + +Las tablas se crean automáticamente al arrancar la aplicación (`Base.metadata.create_all`). + +### 5. Variables de entorno + +Crear `/opt/kirastream/.env`: + +```env +DATABASE_URL=postgresql+asyncpg://kirastream:kirastream_secret_2024@localhost/kirastream +REDIS_URL=redis://localhost:6379/0 +SECRET_KEY=cambia-esto-por-una-clave-segura +ADMIN_USERNAME=admin +ADMIN_PASSWORD=cambia-esto +LOG_LEVEL=INFO +``` + +### 6. Compilar el frontend + +```bash +cd /opt/kirastream/frontend +npm install +npm run build +# Los archivos estáticos se generan en backend/static/ +``` + +### 7. Nginx + +Copiar la configuración al directorio de Nginx: + +```bash +cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak +``` + +Contenido de `/etc/nginx/sites-enabled/default`: + +```nginx +upstream kirastream_backend { + server 127.0.0.1:8000; + keepalive 32; +} + +server { + listen 80 default_server; + server_name _; + + client_max_body_size 20m; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 10s; + + location /panel/ { + alias /opt/kirastream/backend/static/; + try_files $uri $uri/ /panel/index.html; + add_header Cache-Control no-cache; + } + + location = / { return 302 /panel/; } + + location / { + proxy_pass http://kirastream_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection upgrade; + proxy_buffering off; + proxy_cache off; + add_header X-Accel-Buffering no always; + } + + location /panel/assets/ { + alias /opt/kirastream/backend/static/assets/; + expires 30d; + add_header Cache-Control 'public, immutable'; + } + + gzip on; + gzip_types application/json application/javascript text/css text/plain application/x-mpegurl; +} +``` + +```bash +nginx -t && systemctl reload nginx +``` + +### 8. Servicio systemd + +Crear `/etc/systemd/system/kirastream.service`: + +```ini +[Unit] +Description=KiraStream IPTV Server +After=network.target postgresql.service + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/kirastream + +ExecStartPre=/bin/bash -c 'pg_ctlcluster 17 main start 2>/dev/null; true' +ExecStartPre=/bin/bash -c 'redis-cli ping 2>/dev/null || (LANG=C LC_ALL=C redis-server --port 6379 --bind 127.0.0.1 --daemonize yes --dir /var/lib/redis/ && sleep 1); true' +ExecStartPre=/bin/bash -c 'pkill -9 uvicorn 2>/dev/null; sleep 1; true' + +ExecStart=/opt/kirastream/venv/bin/uvicorn backend.main:app \ + --host 0.0.0.0 --port 8000 --workers 1 --log-level info + +StandardOutput=append:/opt/kirastream/logs/backend.log +StandardError=append:/opt/kirastream/logs/backend.log + +Restart=always +RestartSec=5 +KillMode=control-group + +[Install] +WantedBy=multi-user.target +``` + +```bash +mkdir -p /opt/kirastream/logs +systemctl daemon-reload +systemctl enable --now kirastream +``` + +--- + +## Configuración + +Todas las variables se leen de `/opt/kirastream/.env` (o variables de entorno del sistema): + +| Variable | Por defecto | Descripción | +|---|---|---| +| `DATABASE_URL` | `postgresql+asyncpg://kirastream:...@localhost/kirastream` | URL de conexión a PostgreSQL | +| `REDIS_URL` | `redis://localhost:6379/0` | URL de Redis | +| `SECRET_KEY` | *(cambiar)* | Clave para firmar tokens JWT | +| `ALGORITHM` | `HS256` | Algoritmo JWT | +| `ACCESS_TOKEN_EXPIRE_MINUTES` | `10080` (7 días) | TTL de tokens de admin | +| `ADMIN_USERNAME` | `admin` | Usuario del panel de administración | +| `ADMIN_PASSWORD` | `kiraadmin` | Contraseña del panel (**cambiar en producción**) | +| `STREAM_CHUNK_SIZE` | `65536` | Tamaño de chunk en bytes (64 KB) | +| `STREAM_QUEUE_MAXSIZE` | `512` | Chunks máximos en cola por cliente (≈ 32 MB) | +| `STREAM_CONNECT_TIMEOUT` | `10` | Timeout de conexión ffmpeg (segundos) | +| `STREAM_READ_TIMEOUT` | `30` | Timeout de lectura (segundos) | +| `EPG_REFRESH_HOURS` | `12` | Intervalo de actualización de EPG | +| `M3U_CACHE_MINUTES` | `30` | Caché de playlist M3U | +| `LOG_LEVEL` | `INFO` | Nivel de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | + +--- + +## Estructura del proyecto + +``` +/opt/kirastream/ +├── backend/ +│ ├── main.py # Punto de entrada FastAPI, lifespan, routers +│ ├── config.py # Settings (pydantic-settings + .env) +│ ├── database.py # Engine async, Base, init_db() +│ ├── api/ +│ │ ├── auth.py # Login JWT para el panel admin +│ │ ├── xtream.py # API pública compatible Xtream Codes +│ │ └── admin/ +│ │ ├── monitoring.py # WebSocket tiempo real + endpoints slots/streams +│ │ ├── providers.py # CRUD proveedores, sync catálogos, health check +│ │ ├── users.py # CRUD usuarios +│ │ ├── catalogs.py # Catálogos y categorías +│ │ ├── custom_categories.py # Categorías personalizadas +│ │ ├── logs.py # Registro de conexiones y fallos de stream +│ │ ├── epg_admin.py # Gestión EPG +│ │ ├── jellyfin_admin.py # Integración Jellyfin +│ │ └── settings.py # Configuración del servidor +│ ├── core/ +│ │ ├── restream.py # BroadcastGroup: ffmpeg pump + fan-out a clientes +│ │ ├── pool.py # ProviderPool: slots, adquisición y liberación +│ │ ├── health_checker.py # Ping periódico a URLs de proveedor +│ │ ├── epg_manager.py # Descarga y fusión de fuentes XMLTV +│ │ ├── catalog.py # Consultas de catálogo por usuario +│ │ ├── probe.py # ffprobe para info de stream (codec, resolución) +│ │ ├── vod_tracker.py # Seguimiento de sesiones VOD/series +│ │ ├── xtream_client.py # Cliente HTTP para API Xtream de proveedores +│ │ └── jellyfin_client.py # Cliente HTTP para Jellyfin +│ ├── models/ +│ │ ├── provider.py # ProviderAccount, ProviderUrl +│ │ ├── channel.py # Channel (live/movie/series) +│ │ ├── user.py # User +│ │ ├── log.py # ConnectionLog +│ │ ├── stream_event.py # StreamEvent (fallos y recuperaciones) +│ │ ├── epg.py # EPGSource +│ │ ├── custom_category.py # CustomCategory, CustomCategoryItem +│ │ ├── notification.py # Notification +│ │ └── jellyfin.py # JellyfinConfig, JellyfinItem +│ └── static/ # Frontend compilado (servido por Nginx) +├── frontend/ +│ ├── src/ +│ │ ├── pages/ +│ │ │ ├── Dashboard.tsx # Monitorización en tiempo real +│ │ │ ├── Logs.tsx # Registro de conexiones y fallos +│ │ │ ├── Providers.tsx # Gestión de proveedores +│ │ │ ├── Users.tsx # Gestión de usuarios +│ │ │ ├── Catalogs.tsx # Catálogos +│ │ │ ├── CustomCategories.tsx +│ │ │ ├── EPG.tsx +│ │ │ ├── Jellyfin.tsx +│ │ │ └── Settings.tsx +│ │ ├── components/ +│ │ │ └── Layout.tsx # Navegación lateral +│ │ └── lib/ +│ │ └── api.ts # Cliente axios con autenticación JWT +│ ├── package.json +│ └── vite.config.ts +├── nginx/ # Configuración de Nginx +├── scripts/ +│ └── install.sh # Script de arranque manual +├── logs/ # Logs de la aplicación (no versionados) +├── .env # Variables de entorno (no versionado) +└── .gitignore +``` + +--- + +## API + +### Autenticación del panel + +``` +POST /api/admin/auth/token +Content-Type: application/x-www-form-urlencoded + +username=admin&password=... +``` + +Devuelve un token JWT que se incluye en las siguientes peticiones como `Authorization: Bearer `. + +### API Xtream Codes (pública) + +| Endpoint | Descripción | +|---|---| +| `GET /player_api.php?username=&password=&action=` | API principal Xtream | +| `GET /get.php?username=&password=&type=m3u_plus` | Playlist M3U | +| `GET /xmltv.php?username=&password=` | EPG en formato XMLTV | +| `GET /{user}/{pass}/{stream_id}` | Stream en directo | +| `GET /live/{user}/{pass}/{stream_id}` | Stream en directo (ruta alternativa) | +| `GET /movie/{user}/{pass}/{stream_id}.mkv` | VOD | +| `GET /series/{user}/{pass}/{episode_id}.mkv` | Episodio de serie | + +### API de administración + +| Endpoint | Descripción | +|---|---| +| `GET /api/admin/monitoring/streams` | Lista streams activos | +| `GET /api/admin/monitoring/slots` | Estado de slots de proveedor | +| `WS /api/admin/monitoring/ws` | Actualizaciones en tiempo real | +| `POST /api/admin/monitoring/kill` | Cortar conexión de un cliente | +| `GET /api/admin/logs` | Registro de conexiones de usuarios | +| `GET /api/admin/logs/stats` | Estadísticas agregadas | +| `GET /api/admin/logs/events` | Registro de fallos y recuperaciones de stream | +| `DELETE /api/admin/logs` | Limpiar registro de conexiones | +| `DELETE /api/admin/logs/events` | Limpiar registro de fallos | +| `GET /api/admin/providers` | Lista de proveedores | +| `POST /api/admin/providers` | Añadir proveedor | +| `GET /api/admin/users` | Lista de usuarios | +| `POST /api/admin/users` | Crear usuario | +| `GET /api/docs` | Documentación interactiva (Swagger UI) | + +--- + +## Panel de administración + +Accesible en `http:///panel/` + +### Dashboard + +Monitorización en tiempo real vía WebSocket. Muestra por cada stream activo: + +- Canal, proveedor y tipo (Directo / Película / Serie) +- Resolución, FPS, códecs de vídeo y audio, bitrate +- Estado de conexión y número de reconexiones +- Salud A/V y correcciones automáticas +- **Cola cliente** — porcentaje de cola del cliente más lento (0 % = consumo sin retraso; 100 % = cliente bloqueado) +- **Reserva** — capacidad del buffer del servidor (32 MB). Muestra `ok` en reposo (normal) y los segundos de cobertura si hay datos en cola +- Usuarios conectados con opción de cortar conexión individualmente +- Métricas globales: streams activos, slots ocupados, ancho de banda total, CPU de ffmpeg + +### Registro + +Dos pestañas: + +**Conexiones de usuarios** — historial de todas las sesiones con usuario, IP, canal, tipo, duración y datos transferidos. Filtros por usuario, canal, tipo y rango de fechas. + +**Fallos de stream** — registro persistente de eventos del sistema: caídas de proveedor y recuperaciones automáticas. Cada evento incluye canal, proveedor, dominio que falló, mensaje de error y número de intento. Se genera automáticamente; no requiere intervención manual. + +--- + +## Gestión del servicio + +```bash +# Estado +systemctl status kirastream + +# Reiniciar (necesario tras cambios en el backend) +systemctl restart kirastream + +# Ver logs en tiempo real +tail -f /opt/kirastream/logs/backend.log + +# Actualizar código y redeployar +cd /opt/kirastream +git pull +cd frontend && npm run build && cd .. +systemctl restart kirastream +``` + +--- + +## Registro y monitorización + +### Logs de la aplicación + +`/opt/kirastream/logs/backend.log` — log estructurado con nivel, módulo y mensaje. Rotación manual recomendada con `logrotate`. + +### Eventos de stream + +Los fallos y recuperaciones de streams se guardan en la tabla `stream_events` de PostgreSQL y son visibles en el panel en **Registro → Fallos de stream**. + +Tipos de evento: + +| Tipo | Descripción | +|---|---| +| `caida` | El stream falló por primera vez (ffmpeg no pudo conectar o el proveedor cerró la sesión) | +| `recuperado` | Tras varios intentos fallidos, el stream estuvo estable más de 30 segundos | + +### Health check de proveedores + +El checker hace ping a cada URL de proveedor cada 5 minutos llamando a `/player_api.php?action=user_info` y actualiza el estado (`ok` / `error` / `timeout`) y la latencia en milisegundos. El resultado es visible en la sección Proveedores del panel. + +--- + +## Notas técnicas de estabilidad + +### Reconexión sin pantalla negra + +Los proveedores IPTV cierran la sesión cada ~2 minutos como política de gestión de sesiones. Cuando ocurre: + +1. ffmpeg detecta el cierre de la conexión TCP y termina. +2. El loop de Python (`_pump_with_retry`) lanza inmediatamente un nuevo proceso ffmpeg con una conexión fresca al proveedor. +3. **La cola del cliente NO se vacía durante la reconexión.** Los datos en cola (hasta 32 MB) siguen alimentando al reproductor mientras el nuevo ffmpeg arranca. +4. El reproductor no experimenta pantalla negra si la reconexión es más rápida que el buffer local del reproductor. + +> **Por qué no se usa `-reconnect_streamed` en ffmpeg:** ese flag hace que ffmpeg reconecte internamente sin morir. El proveedor re-envía desde el último keyframe (2-5 segundos atrás), lo que produce timestamps PTS hacia atrás que los reproductores interpretan como un rebobinado visible. La reconexión a nivel Python, en cambio, produce siempre una conexión limpia desde el punto de emisión actual. + +### Buffer del servidor + +La cola de cada `BroadcastGroup` tiene capacidad para 512 chunks de 64 KB = **32 MB por stream**. En operación normal la cola está casi vacía (el reproductor consume los datos a la misma velocidad que llegan); eso es correcto y no indica ausencia de protección. La protección real está en que la cola no se vacía activamente durante reconexiones. + +### Fan-out eficiente + +Un único proceso ffmpeg sirve a todos los clientes del mismo canal simultáneamente. Si 10 usuarios ven el mismo canal, solo hay una conexión upstream con el proveedor y un proceso ffmpeg. Los chunks se copian a la cola de cada cliente en el mismo dispatch loop. + +### Evicción de clientes lentos + +Si la cola de un cliente está llena durante más de 30 segundos continuados (red muy lenta o reproductor bloqueado), el cliente es desconectado automáticamente para no perjudicar a otros clientes del mismo stream. + +--- + +## Stack tecnológico + +| Capa | Tecnología | +|---|---| +| Backend | Python 3.13, FastAPI 0.115, Uvicorn 0.34 | +| ORM | SQLAlchemy 2.x async + asyncpg 0.30 | +| Base de datos | PostgreSQL 17 | +| Cache / pub-sub | Redis 6+ (redis-py 5.3) | +| Proxy HTTP | aiohttp 3.12 | +| Procesado de stream | ffmpeg 7.1 (copy mode, sin recodificación) | +| Validación | Pydantic 2.13 | +| Frontend | React 18, TypeScript, Vite 6, Tailwind CSS | +| Servidor web | Nginx (proxy inverso + archivos estáticos) | +| Proceso | systemd |