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,90 @@
|
||||
"""Admin API: category/catalog management."""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...database import get_db
|
||||
from ...models.channel import Category, Channel, ChannelProviderMap, ContentType
|
||||
from ..auth import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/catalogs", tags=["catalogs"])
|
||||
|
||||
|
||||
class CategoryOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
type: ContentType
|
||||
provider_category_id: str | None
|
||||
provider_account_id: int | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class GroupedCategoryOut(BaseModel):
|
||||
name: str
|
||||
type: ContentType
|
||||
category_ids: list[int]
|
||||
provider_count: int # distinct providers that can serve channels in this category
|
||||
|
||||
|
||||
@router.get("/", response_model=list[CategoryOut])
|
||||
async def list_categories(
|
||||
content_type: ContentType | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(get_current_admin),
|
||||
):
|
||||
q = select(Category).order_by(Category.type, Category.name)
|
||||
if content_type:
|
||||
q = q.where(Category.type == content_type)
|
||||
result = await db.execute(q)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/grouped", response_model=list[GroupedCategoryOut])
|
||||
async def list_categories_grouped(
|
||||
content_type: ContentType | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(get_current_admin),
|
||||
):
|
||||
"""Returns categories merged by name+type. Includes how many providers can serve each."""
|
||||
q = select(Category).order_by(Category.type, Category.name)
|
||||
if content_type:
|
||||
q = q.where(Category.type == content_type)
|
||||
cats_result = await db.execute(q)
|
||||
all_cats = cats_result.scalars().all()
|
||||
|
||||
# Group by (name_normalized, type)
|
||||
seen: dict[tuple[str, str], dict] = {}
|
||||
for cat in all_cats:
|
||||
key = (cat.name.strip().lower(), str(cat.type))
|
||||
if key not in seen:
|
||||
seen[key] = {"name": cat.name, "type": cat.type, "category_ids": [], "provider_count": 0}
|
||||
seen[key]["category_ids"].append(cat.id)
|
||||
|
||||
if not seen:
|
||||
return []
|
||||
|
||||
# Map each category_id back to its group key for the count query
|
||||
cat_id_to_key: dict[int, tuple[str, str]] = {}
|
||||
for key, group in seen.items():
|
||||
for cat_id in group["category_ids"]:
|
||||
cat_id_to_key[cat_id] = key
|
||||
|
||||
# Single query: distinct providers per category_id — avoids locale-dependent lower() issues
|
||||
pcount_q = await db.execute(
|
||||
select(
|
||||
Channel.category_id,
|
||||
func.count(func.distinct(ChannelProviderMap.provider_account_id)).label("pcount"),
|
||||
)
|
||||
.join(ChannelProviderMap, ChannelProviderMap.channel_id == Channel.id)
|
||||
.where(Channel.category_id.in_(list(cat_id_to_key.keys())))
|
||||
.group_by(Channel.category_id)
|
||||
)
|
||||
for row in pcount_q:
|
||||
key = cat_id_to_key.get(row.category_id)
|
||||
if key and key in seen:
|
||||
seen[key]["provider_count"] = max(seen[key]["provider_count"], row.pcount)
|
||||
|
||||
return list(seen.values())
|
||||
Reference in New Issue
Block a user