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:
@@ -0,0 +1,87 @@
|
||||
"""Client for consuming Xtream Codes API from provider."""
|
||||
import aiohttp
|
||||
import asyncio
|
||||
from typing import Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XtreamClient:
|
||||
def __init__(self, base_url: str, username: str, password: str):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def _api_url(self) -> str:
|
||||
return f"{self.base_url}/player_api.php"
|
||||
|
||||
async def _get(self, params: dict) -> Any:
|
||||
params = {"username": self.username, "password": self.password, **params}
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(self._api_url(), params=params, ssl=False) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json(content_type=None)
|
||||
|
||||
async def get_user_info(self) -> dict:
|
||||
return await self._get({})
|
||||
|
||||
async def get_live_categories(self) -> list[dict]:
|
||||
return await self._get({"action": "get_live_categories"})
|
||||
|
||||
async def get_live_streams(self, category_id: str | None = None) -> list[dict]:
|
||||
params = {"action": "get_live_streams"}
|
||||
if category_id:
|
||||
params["category_id"] = category_id
|
||||
return await self._get(params)
|
||||
|
||||
async def get_vod_categories(self) -> list[dict]:
|
||||
return await self._get({"action": "get_vod_categories"})
|
||||
|
||||
async def get_vod_streams(self, category_id: str | None = None) -> list[dict]:
|
||||
params = {"action": "get_vod_streams"}
|
||||
if category_id:
|
||||
params["category_id"] = category_id
|
||||
return await self._get(params)
|
||||
|
||||
async def get_series_categories(self) -> list[dict]:
|
||||
return await self._get({"action": "get_series_categories"})
|
||||
|
||||
async def get_series(self, category_id: str | None = None) -> list[dict]:
|
||||
params = {"action": "get_series"}
|
||||
if category_id:
|
||||
params["category_id"] = category_id
|
||||
return await self._get(params)
|
||||
|
||||
async def get_series_info(self, series_id: str) -> dict:
|
||||
return await self._get({"action": "get_series_info", "series_id": series_id})
|
||||
|
||||
async def get_vod_info(self, vod_id: str) -> dict:
|
||||
return await self._get({"action": "get_vod_info", "vod_id": vod_id})
|
||||
|
||||
async def get_epg(self, stream_id: str | None = None) -> dict:
|
||||
params = {"action": "get_short_epg"}
|
||||
if stream_id:
|
||||
params["stream_id"] = stream_id
|
||||
return await self._get(params)
|
||||
|
||||
def build_live_stream_url(self, stream_id: str, ext: str = "ts") -> str:
|
||||
return f"{self.base_url}/{self.username}/{self.password}/{stream_id}.{ext}"
|
||||
|
||||
def build_vod_stream_url(self, stream_id: str, ext: str = "mkv") -> str:
|
||||
return f"{self.base_url}/movie/{self.username}/{self.password}/{stream_id}.{ext}"
|
||||
|
||||
def build_series_stream_url(self, stream_id: str, ext: str = "mkv") -> str:
|
||||
return f"{self.base_url}/series/{self.username}/{self.password}/{stream_id}.{ext}"
|
||||
|
||||
def get_xmltv_url(self) -> str:
|
||||
return f"{self.base_url}/xmltv.php?username={self.username}&password={self.password}"
|
||||
|
||||
async def test_connection(self) -> bool:
|
||||
try:
|
||||
info = await self.get_user_info()
|
||||
return info.get("user_info", {}).get("status") == "Active"
|
||||
except Exception as e:
|
||||
logger.warning(f"Provider connection test failed: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user