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>
268 lines
9.2 KiB
Python
268 lines
9.2 KiB
Python
"""Admin API: user management."""
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
from pydantic import BaseModel
|
|
|
|
from ...database import get_db
|
|
from ...models.user import User, UserCatalogEntry, UserCustomCatalogEntry, UserProviderPreference
|
|
from ...models.channel import Category
|
|
from ...models.provider import ProviderAccount
|
|
from ...models.custom_category import CustomCategory
|
|
from ..auth import get_current_admin
|
|
|
|
router = APIRouter(prefix="/users", tags=["users"])
|
|
|
|
|
|
class UserCreate(BaseModel):
|
|
username: str
|
|
password: str
|
|
max_connections: int = 1
|
|
expiry_date: datetime | None = None
|
|
is_active: bool = True
|
|
is_priority: bool = False
|
|
|
|
|
|
class UserUpdate(BaseModel):
|
|
password: str | None = None
|
|
max_connections: int | None = None
|
|
expiry_date: datetime | None = None
|
|
is_active: bool | None = None
|
|
is_priority: bool | None = None
|
|
|
|
|
|
class UserOut(BaseModel):
|
|
id: int
|
|
username: str
|
|
max_connections: int
|
|
expiry_date: datetime | None
|
|
is_active: bool
|
|
is_priority: bool
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.get("/", response_model=list[UserOut])
|
|
async def list_users(db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(select(User).order_by(User.id))
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.post("/", response_model=UserOut)
|
|
async def create_user(data: UserCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
existing = await db.execute(select(User).where(User.username == data.username))
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(400, f"Username '{data.username}' already exists")
|
|
user = User(
|
|
username=data.username,
|
|
max_connections=data.max_connections,
|
|
expiry_date=data.expiry_date,
|
|
is_active=data.is_active,
|
|
is_priority=data.is_priority,
|
|
password_hash="",
|
|
)
|
|
user.set_password(data.password)
|
|
db.add(user)
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
return user
|
|
|
|
|
|
@router.put("/{user_id}", response_model=UserOut)
|
|
async def update_user(user_id: int, data: UserUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(select(User).where(User.id == user_id))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(404, "User not found")
|
|
if data.password is not None:
|
|
user.set_password(data.password)
|
|
if data.max_connections is not None:
|
|
user.max_connections = data.max_connections
|
|
if data.expiry_date is not None:
|
|
user.expiry_date = data.expiry_date
|
|
if data.is_active is not None:
|
|
user.is_active = data.is_active
|
|
if data.is_priority is not None:
|
|
user.is_priority = data.is_priority
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
return user
|
|
|
|
|
|
@router.delete("/{user_id}")
|
|
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(select(User).where(User.id == user_id))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(404, "User not found")
|
|
await db.delete(user)
|
|
await db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
@router.get("/{user_id}/catalog")
|
|
async def get_user_catalog(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(
|
|
select(UserCatalogEntry)
|
|
.options(selectinload(UserCatalogEntry.category))
|
|
.where(UserCatalogEntry.user_id == user_id)
|
|
)
|
|
entries = result.scalars().all()
|
|
return [
|
|
{"id": e.id, "category_id": e.category_id, "category_name": e.category.name, "type": e.category.type}
|
|
for e in entries
|
|
]
|
|
|
|
|
|
@router.post("/{user_id}/catalog")
|
|
async def assign_categories(user_id: int, category_ids: list[int], db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(select(User).where(User.id == user_id))
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(404, "User not found")
|
|
|
|
# Remove existing entries
|
|
existing = await db.execute(select(UserCatalogEntry).where(UserCatalogEntry.user_id == user_id))
|
|
for entry in existing.scalars().all():
|
|
await db.delete(entry)
|
|
|
|
# Add new entries
|
|
added = 0
|
|
for cat_id in category_ids:
|
|
cat_result = await db.execute(select(Category).where(Category.id == cat_id))
|
|
if cat_result.scalar_one_or_none():
|
|
db.add(UserCatalogEntry(user_id=user_id, category_id=cat_id))
|
|
added += 1
|
|
|
|
await db.commit()
|
|
return {"ok": True, "assigned": added}
|
|
|
|
|
|
@router.delete("/{user_id}/catalog/{category_id}")
|
|
async def remove_category(user_id: int, category_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(
|
|
select(UserCatalogEntry).where(
|
|
UserCatalogEntry.user_id == user_id,
|
|
UserCatalogEntry.category_id == category_id,
|
|
)
|
|
)
|
|
entry = result.scalar_one_or_none()
|
|
if not entry:
|
|
raise HTTPException(404, "Catalog entry not found")
|
|
await db.delete(entry)
|
|
await db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
@router.get("/{user_id}/custom-catalog")
|
|
async def get_user_custom_catalog(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(
|
|
select(UserCustomCatalogEntry.custom_category_id).where(UserCustomCatalogEntry.user_id == user_id)
|
|
)
|
|
return [row[0] for row in result.all()]
|
|
|
|
|
|
@router.post("/{user_id}/custom-catalog")
|
|
async def assign_custom_categories(user_id: int, custom_category_ids: list[int], db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(select(User).where(User.id == user_id))
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(404, "User not found")
|
|
|
|
existing = await db.execute(select(UserCustomCatalogEntry).where(UserCustomCatalogEntry.user_id == user_id))
|
|
for entry in existing.scalars().all():
|
|
await db.delete(entry)
|
|
|
|
added = 0
|
|
for cc_id in custom_category_ids:
|
|
cc_result = await db.execute(select(CustomCategory).where(CustomCategory.id == cc_id))
|
|
if cc_result.scalar_one_or_none():
|
|
db.add(UserCustomCatalogEntry(user_id=user_id, custom_category_id=cc_id))
|
|
added += 1
|
|
|
|
await db.commit()
|
|
return {"ok": True, "assigned": added}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Provider preferences
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ProviderPrefIn(BaseModel):
|
|
provider_account_id: int
|
|
priority: int = 0
|
|
|
|
|
|
@router.get("/{user_id}/providers")
|
|
async def get_user_provider_prefs(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)):
|
|
result = await db.execute(
|
|
select(UserProviderPreference)
|
|
.options(selectinload(UserProviderPreference.provider_account))
|
|
.where(UserProviderPreference.user_id == user_id)
|
|
.order_by(UserProviderPreference.priority)
|
|
)
|
|
prefs = result.scalars().all()
|
|
return [
|
|
{
|
|
"id": p.id,
|
|
"provider_account_id": p.provider_account_id,
|
|
"provider_name": p.provider_account.name,
|
|
"priority": p.priority,
|
|
}
|
|
for p in prefs
|
|
]
|
|
|
|
|
|
@router.post("/{user_id}/providers")
|
|
async def add_user_provider_pref(
|
|
user_id: int, data: ProviderPrefIn, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)
|
|
):
|
|
user_result = await db.execute(select(User).where(User.id == user_id))
|
|
if not user_result.scalar_one_or_none():
|
|
raise HTTPException(404, "User not found")
|
|
|
|
provider_result = await db.execute(select(ProviderAccount).where(ProviderAccount.id == data.provider_account_id))
|
|
if not provider_result.scalar_one_or_none():
|
|
raise HTTPException(404, "Provider account not found")
|
|
|
|
# Upsert
|
|
existing = await db.execute(
|
|
select(UserProviderPreference).where(
|
|
UserProviderPreference.user_id == user_id,
|
|
UserProviderPreference.provider_account_id == data.provider_account_id,
|
|
)
|
|
)
|
|
pref = existing.scalar_one_or_none()
|
|
if pref:
|
|
pref.priority = data.priority
|
|
else:
|
|
pref = UserProviderPreference(
|
|
user_id=user_id,
|
|
provider_account_id=data.provider_account_id,
|
|
priority=data.priority,
|
|
)
|
|
db.add(pref)
|
|
|
|
await db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
@router.delete("/{user_id}/providers/{provider_account_id}")
|
|
async def remove_user_provider_pref(
|
|
user_id: int, provider_account_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)
|
|
):
|
|
result = await db.execute(
|
|
select(UserProviderPreference).where(
|
|
UserProviderPreference.user_id == user_id,
|
|
UserProviderPreference.provider_account_id == provider_account_id,
|
|
)
|
|
)
|
|
pref = result.scalar_one_or_none()
|
|
if not pref:
|
|
raise HTTPException(404, "Preference not found")
|
|
await db.delete(pref)
|
|
await db.commit()
|
|
return {"ok": True}
|