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>
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
"""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"})
|
||||
Reference in New Issue
Block a user