docs: complete technical documentation (architecture, DB schema, ops)

Full reference covering:
- Architecture and design decisions
- PostgreSQL schema (all tables)
- Stream engine internals (ProviderPool, BroadcastGroup, ffmpeg flags)
- Xtream Codes API endpoints
- Admin dashboard features and WebSocket protocol
- EPG and Jellyfin integration
- Nginx + systemd configs
- Environment variables and Python dependencies
- Step-by-step reinstall guide
- Common operations (update, logs, Redis/Postgres inspection)
- Lessons learned (ffmpeg flags to avoid, health thresholds, zapping logic)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joaquin
2026-05-18 01:18:28 +02:00
parent 57c8f6fb64
commit a49f67bbff
+957
View File
@@ -0,0 +1,957 @@
# KiraStream — Documentación Técnica Completa
Proxy IPTV multiusuario con fan-out de streams, API Xtream Codes, EPG, dashboard en tiempo real e integración Jellyfin.
---
## Índice
1. [Arquitectura general](#1-arquitectura-general)
2. [Stack tecnológico](#2-stack-tecnológico)
3. [Esquema de base de datos](#3-esquema-de-base-de-datos)
4. [Motor de streams](#4-motor-de-streams)
5. [API Xtream Codes](#5-api-xtream-codes)
6. [Panel de administración](#6-panel-de-administración)
7. [EPG](#7-epg)
8. [Integración Jellyfin](#8-integración-jellyfin)
9. [Infraestructura y despliegue](#9-infraestructura-y-despliegue)
10. [Variables de entorno](#10-variables-de-entorno)
11. [Reinstalación desde cero](#11-reinstalación-desde-cero)
12. [Operaciones habituales](#12-operaciones-habituales)
13. [Decisiones de diseño y lecciones aprendidas](#13-decisiones-de-diseño-y-lecciones-aprendidas)
---
## 1. Arquitectura general
```
[Apps IPTV — TiviMate, Smarters, etc.]
│ Xtream Codes API
▼
┌───────────────────────────────────────────┐
│ Nginx (puerto 80) │
│ /panel/* → static build React │
│ /* → proxy FastAPI :8000 │
└───────────────────┬───────────────────────┘
│
┌───────────────────▼───────────────────────┐
│ KiraStream (FastAPI :8000) │
│ │
│ ┌─────────────────┐ ┌────────────────┐ │
│ │ API Xtream │ │ API Admin │ │
│ │ /player_api.php │ │ /api/admin/* │ │
│ │ /{u}/{p}/{id} │ │ WebSocket /ws │ │
│ └────────┬────────┘ └───────┬────────┘ │
│ │ │ │
│ ┌────────▼───────────────────▼────────┐ │
│ │ Stream Engine │ │
│ │ ProviderPool BroadcastGroup │ │
│ │ (slots Redis) (fan-out asyncio) │ │
│ │ ffmpeg copy-mode │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────────────────┐ │
│ │ Redis │ │ PostgreSQL 17 │ │
│ │ (estado) │ │ (config + catálogos) │ │
│ └──────────┘ └──────────────────────┘ │
└───────────────────────────────────────────┘
│ Xtream Codes (consume)
▼
[Proveedores IPTV — cuenta1 … cuentaN]
```
### Principios clave
- **Fan-out**: N usuarios viendo el mismo canal → 1 solo slot del proveedor. El motor descarga una vez y distribuye a todos los clientes.
- **Sin transcodificación**: los streams se reenvían bit a bit (copia remux con ffmpeg `-c copy`).
- **Multi-cuenta**: varias cuentas del mismo proveedor para multiplicar conexiones simultáneas.
- **Catálogos por usuario**: cada usuario ve solo las categorías que tiene asignadas.
- **Estado efímero en Redis, configuración en PostgreSQL**.
---
## 2. Stack tecnológico
| Capa | Tecnología | Versión |
|------|-----------|---------|
| Backend | Python + FastAPI + uvicorn | Python 3.13, FastAPI 0.115 |
| ORM | SQLAlchemy async + Alembic | 2.0 |
| Base de datos | PostgreSQL | 17 |
| Cache / estado | Redis | 8 |
| Proceso de stream | ffmpeg | sistema |
| EPG | XMLTV fetch + merge por tvg-id | — |
| Frontend | React 18 + Vite + TailwindCSS | React 18, Vite 6 |
| Proxy inverso | Nginx | sistema |
| Servicio | systemd | — |
---
## 3. Esquema de base de datos
### Tablas principales
```sql
-- Cuentas del proveedor IPTV (una por "lista")
provider_accounts (
id SERIAL PK,
name TEXT,
base_url TEXT, -- p.ej. http://proveedor.com:8080
username TEXT,
password TEXT,
max_connections INT DEFAULT 1,
is_active BOOL DEFAULT true,
last_sync_at TIMESTAMPTZ,
auto_sync_hours INT DEFAULT 0, -- 0 = desactivado
last_sync_categories JSONB -- {live:[1,2], movie:[3], series:[]}
)
-- URLs alternativas por cuenta (failover multi-dominio)
provider_urls (
id SERIAL PK,
provider_account_id INT FK → provider_accounts(id) CASCADE,
url TEXT, -- URL base alternativa
priority INT DEFAULT 0, -- menor = mayor prioridad
is_active BOOL DEFAULT true,
status TEXT DEFAULT 'unknown', -- ok / error / timeout / unknown
response_ms INT,
last_checked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ
)
-- Categorías (importadas desde el proveedor)
categories (
id SERIAL PK,
name TEXT,
type TEXT, -- live / movie / series
provider_category_id TEXT,
provider_account_id INT FK → provider_accounts(id)
)
-- Canales deduplicados
channels (
id SERIAL PK,
name TEXT,
tvg_id TEXT,
tvg_logo TEXT,
stream_id_at_provider TEXT,
category_id INT FK → categories(id),
type TEXT, -- live / movie / series
is_active BOOL DEFAULT true
)
-- Mapa canal ↔ cuenta del proveedor (un canal puede estar en varias cuentas)
channel_provider_map (
id SERIAL PK,
channel_id INT FK → channels(id),
provider_account_id INT FK → provider_accounts(id),
stream_url TEXT
)
-- Usuarios finales (Xtream Codes compatible)
users (
id SERIAL PK,
username TEXT UNIQUE,
password_hash TEXT,
max_connections INT DEFAULT 1,
expiry_date TIMESTAMPTZ,
is_active BOOL DEFAULT true,
is_priority BOOL DEFAULT false, -- puede desalojar streams no-priority
preferred_provider_ids JSONB, -- [1,3] — proveedores preferidos
created_at TIMESTAMPTZ
)
-- Entradas de catálogo por usuario
user_catalog_entries (
id SERIAL PK,
user_id INT FK → users(id),
category_id INT FK → categories(id),
type TEXT
)
-- Categorías personalizadas (no vienen del proveedor)
custom_categories (
id SERIAL PK,
name TEXT,
type TEXT,
display_order INT DEFAULT 0
)
custom_category_channels (
id SERIAL PK,
custom_category_id INT FK,
channel_id INT FK,
display_order INT DEFAULT 0
)
-- Fuentes EPG
epg_sources (
id SERIAL PK,
name TEXT,
url TEXT,
last_fetched_at TIMESTAMPTZ,
is_active BOOL DEFAULT true
)
-- Configuración global
app_settings (
key TEXT PK,
value TEXT
)
-- Configuración Jellyfin
jellyfin_configs (
id SERIAL PK,
name TEXT,
base_url TEXT,
api_key TEXT,
category_name TEXT,
provider_account_id INT FK,
is_active BOOL DEFAULT true,
last_sync_at TIMESTAMPTZ
)
-- Log de actividad
stream_logs (
id SERIAL PK,
user_id INT,
username TEXT,
channel_id INT,
channel_name TEXT,
provider_account_id INT,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
bytes_transferred BIGINT
)
```
---
## 4. Motor de streams
El motor tiene dos capas: **ProviderPool** (gestión de slots) y **BroadcastGroup** (fan-out y proxy).
### 4.1 ProviderPool (`backend/core/pool.py`)
Singleton que gestiona todos los BroadcastGroup activos y la asignación de slots de proveedor.
**Estado en Redis:**
```
ks:stream:{channel_id} → hash {provider_account_id, stream_url, clients, started_at}
ks:slot:{account_id} → channel_id (clave = slot ocupado)
ks:user_conns:{user_id} → set de client_ids activos
ks:monitoring → pub/sub channel para WebSocket
```
**Flujo `acquire(channel_id, user_id, ...)`:**
1. Comprueba límite de conexiones del usuario (Redis + in-memory)
2. Si el canal ya está activo → attach al BroadcastGroup existente
3. Si no → busca slot libre entre las cuentas del proveedor (ordenadas por preferencia del usuario)
4. Si no hay slot libre y el usuario es `is_priority` → evicta el stream con menos clientes sin usuarios priority
5. Reserva slot en Redis, crea BroadcastGroup, arranca stream
6. Devuelve `ClientHandle` con una `asyncio.Queue` personal del cliente
**Flujo `release(channel_id, client_id)`:**
1. Elimina el cliente del grupo
2. Si quedan 0 clientes → para el grupo, libera slot Redis
3. Si quedan clientes → actualiza contador
**Zapping detection:** si un usuario ya tiene una conexión de menos de 10s, la desaloja silenciosamente en lugar de rechazar la nueva. Esto permite cambiar de canal rápido desde una TV sin ver el "stream bloqueado".
**Multi-dominio failover (`_build_stream_urls`):**
Para cada slot asignado, consulta `provider_urls` ordenadas por prioridad y construye una lista de URLs alternativas sustituyendo el dominio base manteniendo el path del stream. El `BroadcastGroup` recibe todas y hace failover automático.
### 4.2 BroadcastGroup (`backend/core/restream.py`)
Descarga un stream del proveedor y lo distribuye a N clientes mediante colas asyncio.
**Arquitectura:**
```
ffmpeg (proceso externo)
stdout → _pump_once → _dispatch → Queue[cliente1]
→ Queue[cliente2]
→ Queue[clienteN]
stderr → _log_ffmpeg_stderr (logging + contador av_desync)
ClientHandle.read() → itera la Queue y yield chunks al StreamingResponse HTTP
```
**Comando ffmpeg (copy-mode):**
```bash
ffmpeg -hide_banner -loglevel warning \
-reconnect 1 \
-reconnect_streamed 1 \
-reconnect_delay_max 2 \
-timeout 10000000 \
-i {stream_url} \
-c copy \
-f mpegts pipe:1
```
- `-reconnect_streamed 1`: al expirar el token 302 del proveedor, ffmpeg reenvía el GET original y obtiene un token nuevo. No se desconectan los clientes.
- `-timeout 10000000`: stall detection de 10 segundos a nivel socket.
- `-c copy`: remux sin transcodificación. Calidad bit a bit idéntica al proveedor.
**Por qué ffmpeg en lugar de aiohttp directo:**
El proveedor usa redirecciones 302 con tokens de sesión de corta duración (típicamente 60-120s para HD, ~28s para algunos 4K). Con aiohttp puro, al expirar el token hay que reconectar a nivel Python, lo que producía desync A/V visible porque el nuevo stream reinicia sus PTS/DTS. ffmpeg con `-reconnect_streamed` renegocia el token internamente y normaliza los PTS/DTS en el remux, haciendo la transición imperceptible para el cliente.
**Flags que NO usar:**
- `-use_wallclock_as_timestamps 1`: reemplaza cada PTS/DTS con el reloj del servidor. Genera una advertencia de stderr por cada paquete con jitter de red, resultando en cientos de warnings por minuto que atascan ffmpeg y producen freezes visibles. **No usar.**
- `-fflags +discardcorrupt`: descarta paquetes corruptos en el punto de reconexión. El problema es que ese paquete puede ser el IDR keyframe que HEVC necesita para arrancar. Sin él, el decoder espera el siguiente IDR (hasta 2s) y el player salta al live edge → salto hacia atrás visible. **No usar.**
- `-avoid_negative_ts make_zero`: útil para VoD pero en live con reconexiones produce acumulación de offsets incorrectos que desincroniza A/V. **No usar para live.**
**Failover multi-URL:**
`_pump_with_retry` itera las URLs del proveedor en orden. Si una falla, prueba la siguiente inmediatamente (sin espera). Solo hace backoff (máx 5s) cuando ha fallado el ciclo completo de todas las URLs.
**Monitor de salud (`_health_check_loop`):**
Tarea asyncio que corre cada 5s y comprueba la antigüedad del último dato recibido:
- `< 12s` → `av_health = "ok"`
- `12-25s` → `av_health = "warning"`
- `> 25s` → `av_health = "error"` + mata ffmpeg vía SIGTERM para forzar reconexión inmediata (cuenta como "corrección")
**Buffer (`_buffer_pct`):**
Porcentaje del queue del cliente más lento. 0% = cliente consume en tiempo real (ideal). 100% = cliente bloqueado o muy lento (riesgo de drops).
**Eviction de clientes lentos:**
Si un cliente lleva más de 30s con la queue llena (sin consumir), es desalojado automáticamente.
### 4.3 HealthChecker (`backend/core/health_checker.py`)
Tarea de fondo que cada 5 minutos comprueba todos los dominios activos de cada proveedor haciendo una petición a `player_api.php?action=user_info`. Actualiza `status`, `response_ms` y `last_checked_at` en la tabla `provider_urls`.
### 4.4 VodTracker (`backend/core/vod_tracker.py`)
Gestiona sesiones VOD/series (sin fan-out, cada cliente es independiente). Hace proxy directo del stream del proveedor sin pasar por ProviderPool, ya que VOD no tiene límite de slots compartidos.
---
## 5. API Xtream Codes
Ruta base: `/` — compatible con TiviMate, IPTV Smarters, IPTV Pro y cualquier app que soporte Xtream Codes.
### Autenticación de usuario
Todos los endpoints de la API pública llevan `?username=X&password=Y` o `/{username}/{password}/`.
### Endpoints
| Endpoint | Función |
|----------|---------|
| `GET /player_api.php?action=user_info` | Info del usuario + límites de conexión |
| `GET /player_api.php?action=get_live_categories` | Categorías live del catálogo del usuario |
| `GET /player_api.php?action=get_live_streams` | Canales live del usuario |
| `GET /player_api.php?action=get_vod_categories` | Categorías VOD |
| `GET /player_api.php?action=get_vod_streams` | Películas |
| `GET /player_api.php?action=get_vod_info&vod_id=X` | Detalle película |
| `GET /player_api.php?action=get_series_categories` | Categorías series |
| `GET /player_api.php?action=get_series` | Series |
| `GET /player_api.php?action=get_series_info&series_id=X` | Detalle serie + episodios |
| `GET /get.php?type=m3u_plus` | Playlist M3U filtrada por catálogo del usuario |
| `GET /xmltv.php` | EPG XMLTV filtrado por canales del usuario |
| `GET /live/{u}/{p}/{stream_id}` | Stream live (proxy con fan-out) |
| `GET /live/{u}/{p}/{stream_id}.ts` | Ídem con extensión |
| `GET /movie/{u}/{p}/{stream_id}.{ext}` | Stream VOD |
| `GET /series/{u}/{p}/{stream_id}.{ext}` | Episodio de serie |
### Lógica de stream live
```
GET /live/usuario/password/840238.ts
→ autenticar usuario
→ resolver canal (stream_id → channel_id)
→ pool.acquire(channel_id, user_id, ...)
→ StreamingResponse(handle.read()) ← HTTP chunked al cliente
→ [on disconnect] pool.release(channel_id, client_id, user_id)
```
---
## 6. Panel de administración
URL: `http://{IP}/panel/`
Credenciales por defecto: `admin` / definido en config.py
### Páginas
#### Dashboard (`/`)
- **6 StatCards**: streams activos, usuarios conectados, slots ocupados, bajada total del proveedor, CPU total ffmpeg, salud A/V global
- **Tabla de streams**: canal, proveedor, codec/resolución detectada, estado de conexión, salud A/V (ok/aviso/error), barra de buffer, usuarios con botón de corte, bitrate ↓/↑, tiempo activo, datos transferidos
- **Estado de slots**: qué proveedor tiene qué canal
- **WebSocket**: actualización en tiempo real (cada 2s + eventos Redis)
#### Proveedores (`/providers`)
- CRUD de cuentas del proveedor (nombre, URL base, usuario, contraseña, max_connections)
- Sync manual: importa categorías y canales desde la API del proveedor
- Auto-sync configurable (cada N horas)
- Multi-dominio: añadir URLs alternativas por proveedor con indicador de salud (ok/error/timeout/unknown + ms)
- Botón "Verificar todos" para forzar health check inmediato
#### Usuarios (`/users`)
- CRUD de usuarios Xtream Codes
- Campos: usuario, contraseña, max_connections, expiry_date, is_priority, proveedores preferidos
- Ver conexiones activas
#### Catálogos (`/catalogs`)
- Asignar categorías del proveedor a cada usuario
- Filtro por tipo (live/movie/series)
#### Categorías personalizadas (`/custom-categories`)
- Crear categorías propias con canales seleccionados manualmente
- Control de orden de visualización
#### EPG (`/epg`)
- Añadir fuentes XMLTV (URL)
- Estado de última importación
#### Jellyfin (`/jellyfin`)
- Configurar instancias Jellyfin
- Sync de biblioteca local como categorías KiraStream
#### Logs (`/logs`)
- Historial de streams: usuario, canal, proveedor, duración, bytes
#### Configuración (`/settings`)
- Variables de configuración globales editables desde el panel
### WebSocket de monitoring
```
WS /api/admin/monitoring/ws
→ Server emite en dos situaciones:
1. Cada 2s (broadcast general)
2. En cada cambio de estado (vía Redis pub/sub)
Payload:
{
"type": "state" | "update",
"data": [
{
"stream_type": "live",
"channel_id": 310412,
"channel_name": "ES: LA 1 UHD",
"provider_name": "Lista1",
"client_count": 2,
"clients": [{"client_id": "...", "username": "joaquin"}],
"started_at": 1779000000.0,
"bytes_pumped": 125000000,
"bps_down": 3200000,
"bps_up": 6400000,
"status": "ok",
"reconnect_count": 3,
"last_data_ago": 0.3,
"stream_info": {"video_codec": "hevc", "width": 3840, "height": 2160, ...},
"ffmpeg_pid": 3251,
"ffmpeg_cpu_pct": 0.38,
"av_health": "ok",
"av_desync_count": 12,
"corrections": 0,
"buffer_pct": 0.0,
"active_url_domain": "proveedor.com",
"url_count": 3
}
]
}
```
---
## 7. EPG
- **Fuentes**: URLs XMLTV configuradas en el panel
- **Merge**: por `tvg-id`. Si varios canales tienen el mismo tvg-id, se unifica la EPG
- **Refresco**: tarea de fondo cada N horas (configurable, por defecto 12h)
- **Endpoint cliente**: `GET /xmltv.php?username=X&password=Y` — devuelve EPG filtrada por los canales del catálogo del usuario
---
## 8. Integración Jellyfin
- Configura una o varias instancias Jellyfin con su URL base y API key
- Al hacer sync, importa la biblioteca local de Jellyfin como una categoría adicional en KiraStream
- Los streams de Jellyfin se sirven como VOD a través de KiraStream (proxy transparente)
- Los usuarios ven el contenido de Jellyfin mezclado con el IPTV dentro de la misma app cliente
---
## 9. Infraestructura y despliegue
### Entorno
- **Contenedor**: Proxmox LXC, PCT 117, hostname `kiraIPTV`, IP `10.10.10.234`
- **OS**: Debian 13 Trixie
- **Recursos**: 4 cores, 8 GB RAM, 50 GB ZFS local
### Estructura en disco
```
/opt/kirastream/
├── backend/
│ ├── __init__.py
│ ├── main.py # Punto de entrada FastAPI
│ ├── config.py # Settings (pydantic-settings)
│ ├── database.py # Engine PostgreSQL + init_db()
│ ├── api/
│ │ ├── auth.py # JWT admin
│ │ ├── xtream.py # API Xtream Codes pública
│ │ └── admin/
│ │ ├── providers.py # CRUD proveedores + sync + multi-URL
│ │ ├── users.py # CRUD usuarios
│ │ ├── catalogs.py # Catálogos por usuario
│ │ ├── custom_categories.py
│ │ ├── epg_admin.py # Fuentes EPG
│ │ ├── monitoring.py # REST + WebSocket monitoring
│ │ ├── logs.py # Historial de streams
│ │ ├── settings.py # Config global
│ │ └── jellyfin_admin.py
│ ├── core/
│ │ ├── pool.py # ProviderPool (slots + Redis)
│ │ ├── restream.py # BroadcastGroup (fan-out + ffmpeg)
│ │ ├── xtream_client.py # Cliente HTTP para API del proveedor
│ │ ├── epg_manager.py # Fetch + merge XMLTV
│ │ ├── catalog.py # Resolver catálogo por usuario
│ │ ├── health_checker.py # Health check de dominios del proveedor
│ │ ├── vod_tracker.py # Sesiones VOD/series
│ │ ├── probe.py # ffprobe para detectar codec/resolución
│ │ └── jellyfin_client.py
│ ├── models/
│ │ ├── provider.py # ProviderAccount, ProviderUrl
│ │ ├── channel.py # Channel, Category, ChannelProviderMap
│ │ ├── user.py # User, UserCatalogEntry
│ │ ├── epg.py # EpgSource
│ │ ├── custom_category.py
│ │ ├── log.py # StreamLog
│ │ ├── notification.py
│ │ └── jellyfin.py
│ └── static/ # Frontend compilado (generado por npm run build)
│ ├── index.html
│ └── assets/
│ ├── index-*.js
│ └── index-*.css
├── frontend/
│ ├── src/
│ │ ├── App.tsx
│ │ ├── main.tsx
│ │ ├── lib/api.ts # Axios con base /api/admin
│ │ ├── components/
│ │ │ └── Layout.tsx
│ │ └── pages/
│ │ ├── Dashboard.tsx # Monitoring tiempo real
│ │ ├── Providers.tsx # Multi-URL, health badges
│ │ ├── Users.tsx
│ │ ├── Catalogs.tsx
│ │ ├── CustomCategories.tsx
│ │ ├── EPG.tsx
│ │ ├── Jellyfin.tsx
│ │ ├── Logs.tsx
│ │ ├── Settings.tsx
│ │ └── Login.tsx
│ ├── package.json
│ ├── vite.config.ts
│ └── tailwind.config.js
├── nginx/
│ └── kirastream.conf
├── logs/
│ └── backend.log
├── venv/ # Virtualenv Python
└── README.md
```
### Nginx (`/etc/nginx/sites-enabled/kirastream`)
```nginx
upstream kirastream_backend {
server 127.0.0.1:8000;
keepalive 32;
}
server {
listen 80 default_server;
server_name _;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 10s;
# Panel de administración
location /panel {
alias /opt/kirastream/backend/static;
try_files $uri $uri/ /panel/index.html;
}
# API y Xtream Codes
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
add_header X-Accel-Buffering no;
}
# WebSocket monitoring (timeout extra largo)
location /api/admin/monitoring/ws {
proxy_pass http://kirastream_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
}
# Assets del frontend (cache inmutable)
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;
access_log /var/log/nginx/kirastream_access.log;
error_log /var/log/nginx/kirastream_error.log;
}
```
### Systemd (`/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 || (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
```
---
## 10. Variables de entorno
Fichero: `/opt/kirastream/.env` (si no existe, se usan los defaults de `config.py`)
```env
# Base de datos
DATABASE_URL=postgresql+asyncpg://kirastream:PASSWORD@localhost/kirastream
# Redis
REDIS_URL=redis://localhost:6379/0
# JWT — generar con: openssl rand -hex 32
SECRET_KEY=CAMBIAR_ESTO
# Panel admin
ADMIN_USERNAME=admin
ADMIN_PASSWORD=CAMBIAR_ESTO
# Stream (opcionales)
STREAM_CHUNK_SIZE=65536 # bytes por chunk (64 KB)
STREAM_QUEUE_MAXSIZE=512 # chunks en queue por cliente
STREAM_CONNECT_TIMEOUT=10 # segundos
STREAM_READ_TIMEOUT=30 # segundos
# EPG
EPG_REFRESH_HOURS=12
# Logging
LOG_LEVEL=INFO # DEBUG / INFO / WARNING / ERROR
```
### Dependencias Python (`pip freeze` actual)
```
aiofiles==24.1.0
aiohttp==3.12.4
alembic==1.16.1
asyncpg==0.30.0
bcrypt==4.3.0
fastapi==0.115.12
hiredis==3.3.1
httpx==0.28.1
lxml==5.4.0
passlib==1.7.4
pydantic==2.13.4
pydantic-settings==2.9.1
PyJWT==2.9.0
python-jose==3.3.0
python-multipart==0.0.20
redis==5.3.0
SQLAlchemy==2.0.41
structlog==25.3.0
tenacity==9.1.2
uvicorn==0.34.3
uvloop==0.22.1
websockets==15.0.1
```
---
## 11. Reinstalación desde cero
### Paso 1 — Preparar el sistema
```bash
# En el host Proxmox, acceder al contenedor
pct exec 117 -- bash
# Instalar dependencias del sistema
apt update && apt install -y \
postgresql-17 \
redis-server \
nginx \
ffmpeg \
python3.13 python3.13-venv \
nodejs npm \
git curl
# Verificar ffmpeg
ffmpeg -version | head -1
```
### Paso 2 — Clonar repositorio
```bash
git clone https://git.kiracloud.es/joaquin/KiraTV.git /opt/kirastream
cd /opt/kirastream
```
### Paso 3 — Base de datos PostgreSQL
```bash
# Crear usuario y base de datos
sudo -u postgres psql <<EOF
CREATE USER kirastream WITH PASSWORD 'ELEGIR_PASSWORD';
CREATE DATABASE kirastream OWNER kirastream;
GRANT ALL PRIVILEGES ON DATABASE kirastream TO kirastream;
EOF
```
### Paso 4 — Entorno Python
```bash
cd /opt/kirastream
python3.13 -m venv venv
venv/bin/pip install --upgrade pip
# Instalar dependencias (usar el requirements.txt del repo o la lista del apartado 10)
venv/bin/pip install \
fastapi uvicorn[standard] uvloop \
sqlalchemy[asyncio] asyncpg alembic \
pydantic-settings \
redis hiredis \
aiohttp aiofiles httpx \
passlib[bcrypt] python-jose[cryptography] python-multipart PyJWT \
lxml structlog tenacity
```
### Paso 5 — Variables de entorno
```bash
cat > /opt/kirastream/.env <<EOF
DATABASE_URL=postgresql+asyncpg://kirastream:ELEGIR_PASSWORD@localhost/kirastream
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=$(openssl rand -hex 32)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=ELEGIR_PASSWORD
EOF
```
### Paso 6 — Inicializar base de datos
```bash
cd /opt/kirastream
# La función init_db() en database.py crea todas las tablas automáticamente
# al primer arranque. También puede ejecutarse manualmente:
venv/bin/python -c "
import asyncio
from backend.database import init_db
asyncio.run(init_db())
"
```
### Paso 7 — Compilar frontend
```bash
cd /opt/kirastream/frontend
npm install
npm run build
# Los archivos compilados van a /opt/kirastream/backend/static/
```
### Paso 8 — Nginx
```bash
# Copiar configuración
cp /opt/kirastream/nginx/kirastream.conf /etc/nginx/sites-available/kirastream
ln -sf /etc/nginx/sites-available/kirastream /etc/nginx/sites-enabled/kirastream
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
```
### Paso 9 — Systemd
```bash
cp /opt/kirastream/kirastream.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable kirastream
systemctl start kirastream
# Verificar
systemctl status kirastream
tail -f /opt/kirastream/logs/backend.log
```
### Paso 10 — Verificación
```bash
# API health
curl http://localhost:8000/health
# Panel admin
# Abrir en navegador: http://10.10.10.234/panel/
# Logs en vivo
tail -f /opt/kirastream/logs/backend.log
```
---
## 12. Operaciones habituales
### Actualizar desde git
```bash
# En el host Proxmox
cd /tmp/kirastream
git pull origin main
# Push de archivos modificados al contenedor
pct push 117 backend/core/restream.py /opt/kirastream/backend/core/restream.py
pct push 117 backend/core/pool.py /opt/kirastream/backend/core/pool.py
# etc.
# Si hay cambios en el frontend
pct exec 117 -- bash -c "cd /opt/kirastream/frontend && npm run build"
# Reiniciar backend
pct exec 117 -- systemctl restart kirastream
```
### Ver logs
```bash
# Logs en vivo
pct exec 117 -- tail -f /opt/kirastream/logs/backend.log
# Últimas 100 líneas
pct exec 117 -- tail -100 /opt/kirastream/logs/backend.log
# Solo errores
pct exec 117 -- grep ERROR /opt/kirastream/logs/backend.log | tail -20
# Logs de ffmpeg de un canal concreto
pct exec 117 -- grep "ch310412" /opt/kirastream/logs/backend.log | tail -30
```
### Redis — inspección de estado
```bash
pct exec 117 -- redis-cli
# Streams activos
KEYS ks:stream:*
HGETALL ks:stream:310412
# Slots ocupados
KEYS ks:slot:*
# Conexiones de un usuario
SMEMBERS ks:user_conns:2
```
### PostgreSQL — consultas útiles
```bash
pct exec 117 -- sudo -u postgres psql kirastream
-- Usuarios activos
SELECT username, max_connections, expiry_date FROM users WHERE is_active;
-- Canales por categoría
SELECT c.name, count(*) FROM channels ch
JOIN categories c ON ch.category_id = c.id GROUP BY c.name ORDER BY count DESC;
-- Salud de dominios
SELECT pa.name, pu.url, pu.status, pu.response_ms
FROM provider_urls pu JOIN provider_accounts pa ON pa.id = pu.provider_account_id
ORDER BY pa.name, pu.priority;
```
### Reinicio limpio
```bash
# Para todos los streams activos y reinicia
pct exec 117 -- systemctl restart kirastream
# Si Redis tiene estado corrupto
pct exec 117 -- redis-cli FLUSHDB
pct exec 117 -- systemctl restart kirastream
```
---
## 13. Decisiones de diseño y lecciones aprendidas
### Por qué una sola instancia uvicorn (workers=1)
El `ProviderPool` y los `BroadcastGroup` son objetos en memoria con `asyncio.Lock`. Si hubiera múltiples workers, cada proceso tendría su propio pool y se perdería la coordinación de fan-out (dos workers podrían abrir dos conexiones al proveedor para el mismo canal). El estado de coordinación está en Redis para soportar múltiples procesos en el futuro, pero la implementación actual usa un solo worker con asyncio concurrente, que es suficiente para cientos de streams simultáneos dado que la operación es mayoritariamente I/O-bound.
### Por qué ffmpeg en lugar de aiohttp directo
Los proveedores Xtream usan redirecciones 302 con tokens de sesión de corta duración (60-120s para HD, ~28s para algunos streams 4K). Con aiohttp puro, al expirar el token hay que reconectar a nivel Python interrumpiendo el stream. ffmpeg con `-reconnect_streamed 1` renegocia el token internamente sin interrumpir el pipe de stdout, haciendo la transición invisible para el cliente.
### Tokens 302 y freezes en 4K
Algunos canales 4K tienen tokens que expiran cada ~28 segundos. ffmpeg los renegocia en <1 segundo típicamente. El player IPTV debe tener suficiente buffer para cubrir esa brecha (TiviMate por defecto tiene 3-8s). Si el freeze persiste, revisar la velocidad de red entre KiraStream y el proveedor.
### Flags ffmpeg que NO funcionan para live IPTV
| Flag | Problema |
|------|---------|
| `-use_wallclock_as_timestamps 1` | Genera una advertencia de stderr por cada paquete con jitter de red (cientos por minuto), atascando ffmpeg y produciendo freezes |
| `-fflags +discardcorrupt` | Descarta el IDR keyframe del punto de reconexión; el decoder HEVC espera hasta el siguiente IDR (hasta 2s) → salto visible al live edge |
| `-avoid_negative_ts make_zero` | Acumula offsets incorrectos en reconexiones con EAC3, dejando A/V desincronizado ~350ms |
### Umbrales del monitor de salud
Los proveedores renuevan tokens y la reconexión de ffmpeg tarda 1-4 segundos. Con umbrales demasiado bajos (warning >5s) el dashboard marca "aviso" en cada reconexión normal. Los umbrales correctos son:
- `warning`: sin datos >12s
- `error` + auto-corrección: sin datos >25s
### Zapping vs multi-dispositivo
La lógica de `_check_user_connections` distingue entre cambiar de canal rápido ("zapping": conexión existente <10s de antigüedad → evict silencioso) y uso multi-dispositivo genuino (conexión >10s → rechaza con error para mostrar "stream bloqueado" en el nuevo dispositivo).
### Fan-out y límites de memoria
Con `STREAM_QUEUE_MAXSIZE=512` y `STREAM_CHUNK_SIZE=65536` bytes, cada cliente tiene un buffer de servidor de hasta 32 MB. Para 25 Mbps (4K) eso es ~10 segundos de buffer máximo. En operación normal el buffer está casi vacío (el cliente consume al ritmo del proveedor). Si `buffer_pct` supera 70% de forma sostenida, el cliente tiene problemas de red o el dispositivo es lento.