b969b7e5af
- 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>
88 lines
3.4 KiB
Python
88 lines
3.4 KiB
Python
"""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
|