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>
116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
"""Background health checker for provider URLs."""
|
|
import asyncio
|
|
import logging
|
|
import time
|
|
from datetime import datetime, timezone
|
|
|
|
import aiohttp
|
|
from sqlalchemy import select
|
|
|
|
from ..database import AsyncSessionLocal
|
|
from ..models.provider import ProviderAccount, ProviderUrl
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CHECK_INTERVAL = 300 # 5 minutes
|
|
|
|
|
|
async def _check_url(base_url: str, username: str, password: str) -> tuple[str, int | None]:
|
|
url = f"{base_url.rstrip('/')}/player_api.php"
|
|
params = {"username": username, "password": password, "action": "user_info"}
|
|
start = time.time()
|
|
try:
|
|
timeout = aiohttp.ClientTimeout(total=8)
|
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
async with session.get(url, params=params, ssl=False) as resp:
|
|
elapsed_ms = int((time.time() - start) * 1000)
|
|
if resp.status == 200:
|
|
try:
|
|
data = await resp.json(content_type=None)
|
|
if "user_info" in data:
|
|
return "ok", elapsed_ms
|
|
except Exception:
|
|
pass
|
|
return "error", elapsed_ms
|
|
except asyncio.TimeoutError:
|
|
return "timeout", None
|
|
except Exception:
|
|
return "error", None
|
|
|
|
|
|
async def check_provider_urls(provider_account_id: int) -> list[dict]:
|
|
"""Check all URLs for a specific provider. Returns list of result dicts."""
|
|
async with AsyncSessionLocal() as db:
|
|
result = await db.execute(
|
|
select(ProviderUrl.id, ProviderUrl.url, ProviderAccount.username, ProviderAccount.password)
|
|
.join(ProviderAccount)
|
|
.where(
|
|
ProviderUrl.provider_account_id == provider_account_id,
|
|
ProviderUrl.is_active == True, # noqa: E712
|
|
)
|
|
)
|
|
rows = result.all()
|
|
|
|
if not rows:
|
|
return []
|
|
|
|
tasks = [_check_url(url, username, password) for _, url, username, password in rows]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
output = []
|
|
async with AsyncSessionLocal() as db:
|
|
for (url_id, url, _, _), check_result in zip(rows, results):
|
|
if isinstance(check_result, Exception):
|
|
status, response_ms = "error", None
|
|
else:
|
|
status, response_ms = check_result
|
|
pu = await db.get(ProviderUrl, url_id)
|
|
if pu:
|
|
pu.status = status
|
|
pu.response_ms = response_ms
|
|
pu.last_checked_at = now
|
|
output.append({"id": url_id, "url": url, "status": status, "response_ms": response_ms})
|
|
await db.commit()
|
|
|
|
return output
|
|
|
|
|
|
async def health_check_loop() -> None:
|
|
"""Runs forever, checking all active provider URLs every CHECK_INTERVAL seconds."""
|
|
logger.info("Provider URL health checker started")
|
|
while True:
|
|
await asyncio.sleep(CHECK_INTERVAL)
|
|
try:
|
|
async with AsyncSessionLocal() as db:
|
|
result = await db.execute(
|
|
select(ProviderUrl.id, ProviderUrl.url, ProviderAccount.username, ProviderAccount.password)
|
|
.join(ProviderAccount)
|
|
.where(ProviderUrl.is_active == True) # noqa: E712
|
|
)
|
|
rows = result.all()
|
|
|
|
if not rows:
|
|
continue
|
|
|
|
tasks = [_check_url(url, username, password) for _, url, username, password in rows]
|
|
check_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
async with AsyncSessionLocal() as db:
|
|
for (url_id, _, _, _), check_result in zip(rows, check_results):
|
|
if isinstance(check_result, Exception):
|
|
status, response_ms = "error", None
|
|
else:
|
|
status, response_ms = check_result
|
|
pu = await db.get(ProviderUrl, url_id)
|
|
if pu:
|
|
pu.status = status
|
|
pu.response_ms = response_ms
|
|
pu.last_checked_at = now
|
|
await db.commit()
|
|
|
|
logger.info(f"Health check done: {len(rows)} URLs checked")
|
|
except Exception as e:
|
|
logger.error(f"Health check loop error: {e}")
|