Files
KiraTV/backend/core/epg_manager.py
T
joaquin f5d6745ed0 Initial commit: KiraTV IPTV proxy server
Full-stack IPTV management system with Xtream Codes API compatibility,
multi-account provider fan-out, Jellyfin local library integration,
real-time dashboard, EPG support, and A/V-sync-safe stream recovery.
2026-05-17 23:22:05 +02:00

99 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 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=asyncio.get_event_loop().time()) # 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)