Files
KiraTV/backend/main.py
T
KiraStream b969b7e5af Mejoras de estabilidad de stream IPTV y nuevo registro de fallos
- restream.py: eliminar -reconnect_streamed de ffmpeg para evitar rebobinados
  al reconectar (el proveedor re-enviaba desde keyframe anterior causando PTS
  backward). El loop de Python gestiona las reconexiones con conexión fresca.
- restream.py: eliminar _drain_queue del loop de reconexión para que el buffer
  de la cola cubra el tiempo de reconexión sin pantalla negra en el player.
- restream.py: añadir buffer_server_bytes/secs/capacity a stats() y método
  _log_stream_event() para persistir eventos de caída y recuperación en BD.
- models/stream_event.py: nuevo modelo StreamEvent para registro de fallos.
- database.py: registrar StreamEvent en init_db.
- api/admin/logs.py: nuevos endpoints GET/DELETE /logs/events.
- Dashboard.tsx: mostrar reserva de buffer del servidor (capacidad + estado).
- Logs.tsx: añadir pestaña "Fallos de stream" con tabla de eventos persistidos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:43:31 +00:00

144 lines
5.4 KiB
Python

"""KiraStream — IPTV Management Server."""
import asyncio
import logging
import sys
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm
import json
from .config import settings
from .database import init_db, AsyncSessionLocal
from .core.pool import pool
from .core.epg_manager import epg_background_task
from .core.health_checker import health_check_loop
from .database import get_db
logging.basicConfig(
level=getattr(logging, settings.log_level),
format="%(asctime)s %(levelname)s %(name)s — %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)
async def _auto_sync_loop() -> None:
"""Check every 30 minutes if any provider is due for auto-sync."""
from sqlalchemy import select
from .models.provider import ProviderAccount
from .api.admin.providers import _run_sync, _sync_status
await asyncio.sleep(60) # delay first check so startup completes
while True:
try:
async with AsyncSessionLocal() as db:
result = await db.execute(
select(ProviderAccount).where(ProviderAccount.auto_sync_hours > 0, ProviderAccount.is_active == True) # noqa: E712
)
providers = result.scalars().all()
now = asyncio.get_event_loop().time()
import time as _time
wall_now = _time.time()
for p in providers:
interval_secs = p.auto_sync_hours * 3600
last_sync_ts = p.last_sync_at.timestamp() if p.last_sync_at else 0
if wall_now - last_sync_ts >= interval_secs:
if _sync_status.get(p.id, {}).get("status") == "running":
continue
logger.info(f"Auto-sync triggered for provider {p.id} ({p.name})")
cats = json.loads(p.last_sync_categories) if p.last_sync_categories else {}
asyncio.create_task(_run_sync(
p.id, p.base_url, p.username, p.password,
selected_live=set(cats["live"]) if cats.get("live") is not None else None,
selected_movie=set(cats["movie"]) if cats.get("movie") is not None else None,
selected_series=set(cats["series"]) if cats.get("series") is not None else None,
))
except Exception as e:
logger.error(f"Auto-sync loop error: {e}")
await asyncio.sleep(1800) # check every 30 minutes
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("KiraStream starting up...")
await init_db()
await pool.init()
epg_task = asyncio.create_task(epg_background_task(get_db))
auto_sync_task = asyncio.create_task(_auto_sync_loop())
health_task = asyncio.create_task(health_check_loop())
logger.info("KiraStream ready")
yield
logger.info("KiraStream shutting down...")
await pool.close()
epg_task.cancel()
auto_sync_task.cancel()
health_task.cancel()
for t in (epg_task, auto_sync_task, health_task):
try:
await t
except asyncio.CancelledError:
pass
app = FastAPI(
title="KiraStream",
description="Professional IPTV Management with Xtream Codes",
version="1.0.0",
lifespan=lifespan,
docs_url="/api/docs",
redoc_url="/api/redoc",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Admin auth ---
from .api.auth import login, Token # noqa: E402
@app.post("/api/admin/auth/token", response_model=Token, tags=["admin"])
async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
return await login(form_data)
# --- Admin routers ---
from .api.admin.providers import router as providers_router # noqa: E402
from .api.admin.users import router as users_router # noqa: E402
from .api.admin.catalogs import router as catalogs_router # noqa: E402
from .api.admin.epg_admin import router as epg_admin_router # noqa: E402
from .api.admin.monitoring import router as monitoring_router # noqa: E402
from .api.admin.custom_categories import router as custom_cats_router # noqa: E402
from .api.admin.logs import router as logs_router # noqa: E402
from .api.admin.settings import router as settings_router # noqa: E402
from .api.admin.jellyfin_admin import router as jellyfin_router # noqa: E402
app.include_router(providers_router, prefix="/api/admin")
app.include_router(users_router, prefix="/api/admin")
app.include_router(catalogs_router, prefix="/api/admin")
app.include_router(epg_admin_router, prefix="/api/admin")
app.include_router(monitoring_router, prefix="/api/admin")
app.include_router(custom_cats_router, prefix="/api/admin")
app.include_router(logs_router, prefix="/api/admin")
app.include_router(settings_router, prefix="/api/admin")
app.include_router(jellyfin_router, prefix="/api/admin")
# --- Xtream Codes compatible API (public) ---
from .api.xtream import router as xtream_router # noqa: E402
app.include_router(xtream_router)
@app.get("/health")
async def health():
return {"status": "ok", "service": "kirastream"}
@app.get("/")
async def root():
return JSONResponse({"message": "KiraStream IPTV Server", "docs": "/api/docs"})