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>
79 lines
2.8 KiB
Python
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())
|