"""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'' 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)