Files
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

100 lines
3.3 KiB
Python

"""EPG manager: fetch XMLTV sources, merge by tvg-id, serve filtered by user catalog."""
import asyncio
import gzip
import logging
import time
from datetime import datetime, timezone
from io import BytesIO
from lxml import etree
import aiohttp
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from ..models.epg import EpgSource
from ..config import settings
logger = logging.getLogger(__name__)
_merged_epg: bytes = b""
_last_update: float = 0.0
_update_lock = asyncio.Lock()
async def refresh_epg(db: AsyncSession) -> None:
global _merged_epg, _last_update
async with _update_lock:
result = await db.execute(select(EpgSource).where(EpgSource.is_active == True)) # noqa: E712
sources = result.scalars().all()
if not sources:
logger.info("No active EPG sources configured")
return
root = etree.Element("tv")
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession(timeout=timeout) as session:
for source in sources:
try:
data = await _fetch_xmltv(session, source.url)
tree = etree.fromstring(data)
for elem in tree:
root.append(elem)
logger.info(f"EPG loaded from {source.name}: {len(tree)} entries")
except Exception as e:
logger.error(f"EPG fetch failed for {source.name}: {e}")
_merged_epg = etree.tostring(root, xml_declaration=True, encoding="UTF-8")
_last_update = time.time()
from sqlalchemy import update
await db.execute(
update(EpgSource).where(EpgSource.is_active == True).values(last_fetched_at=datetime.now(timezone.utc)) # noqa: E712
)
await db.commit()
logger.info(f"EPG merge complete, {len(_merged_epg)} bytes")
async def _fetch_xmltv(session: aiohttp.ClientSession, url: str) -> bytes:
async with session.get(url, ssl=False) as resp:
resp.raise_for_status()
data = await resp.read()
if data[:2] == b"\x1f\x8b":
data = gzip.decompress(data)
return data
def get_merged_epg() -> bytes:
return _merged_epg
def filter_epg_for_channels(tvg_ids: set[str]) -> bytes:
"""Return EPG XML filtered to only contain channels/programs for given tvg-ids."""
if not _merged_epg:
return b'<?xml version="1.0" encoding="UTF-8"?><tv></tv>'
try:
tree = etree.fromstring(_merged_epg)
root = etree.Element("tv")
for elem in tree:
tag = elem.tag
if tag == "channel":
if elem.get("id") in tvg_ids:
root.append(elem)
elif tag == "programme":
if elem.get("channel") in tvg_ids:
root.append(elem)
return etree.tostring(root, xml_declaration=True, encoding="UTF-8")
except Exception as e:
logger.error(f"EPG filter error: {e}")
return _merged_epg
async def epg_background_task(get_db_func) -> None:
"""Periodic EPG refresh task — runs every N hours."""
while True:
try:
async for db in get_db_func():
await refresh_epg(db)
except Exception as e:
logger.error(f"EPG background refresh error: {e}")
await asyncio.sleep(settings.epg_refresh_hours * 3600)