Files
KiraStream b969b7e5af 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>
2026-05-19 14:43:31 +00:00

79 lines
2.8 KiB
Python

"""Resolve channel catalog for a given user based on their assigned categories."""
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from ..models.user import User, UserCatalogEntry, UserProviderPreference
from ..models.channel import Channel, Category, ChannelProviderMap, ContentType
from ..models.provider import ProviderAccount
async def get_user_categories(db: AsyncSession, user: User, content_type: ContentType | None = None) -> list[Category]:
q = (
select(Category)
.join(UserCatalogEntry, UserCatalogEntry.category_id == Category.id)
.where(UserCatalogEntry.user_id == user.id)
)
if content_type:
q = q.where(Category.type == content_type)
result = await db.execute(q)
return list(result.scalars().all())
async def get_user_channels(
db: AsyncSession,
user: User,
content_type: ContentType | None = None,
category_id: int | None = None,
) -> list[Channel]:
category_ids_q = (
select(UserCatalogEntry.category_id)
.where(UserCatalogEntry.user_id == user.id)
)
q = (
select(Channel)
.options(selectinload(Channel.provider_maps), selectinload(Channel.category))
.where(Channel.category_id.in_(category_ids_q))
.where(Channel.is_active == True) # noqa: E712
)
if content_type:
q = q.where(Channel.type == content_type)
if category_id:
q = q.where(Channel.category_id == category_id)
result = await db.execute(q)
return list(result.scalars().all())
async def get_channel_provider_maps(db: AsyncSession, channel_id: int) -> list[dict]:
q = (
select(ChannelProviderMap, ProviderAccount.name.label("provider_name"))
.join(ProviderAccount, ProviderAccount.id == ChannelProviderMap.provider_account_id)
.where(ChannelProviderMap.channel_id == channel_id)
)
result = await db.execute(q)
rows = result.all()
return [
{
"provider_account_id": row.ChannelProviderMap.provider_account_id,
"stream_url": row.ChannelProviderMap.stream_url,
"provider_name": row.provider_name or f"#{row.ChannelProviderMap.provider_account_id}",
}
for row in rows
]
async def can_user_access_channel(db: AsyncSession, user: User, channel_id: int) -> bool:
channels = await get_user_channels(db, user)
return any(ch.id == channel_id for ch in channels)
async def get_user_preferred_provider_ids(db: AsyncSession, user: User) -> list[int]:
result = await db.execute(
select(UserProviderPreference.provider_account_id)
.where(UserProviderPreference.user_id == user.id)
.order_by(UserProviderPreference.priority)
)
return list(result.scalars().all())