f5d6745ed0
Full-stack IPTV management system with Xtream Codes API compatibility, multi-account provider fan-out, Jellyfin local library integration, real-time dashboard, EPG support, and A/V-sync-safe stream recovery.
141 lines
5.3 KiB
Python
141 lines
5.3 KiB
Python
"""KiraStream — IPTV Management Server."""
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.security import OAuth2PasswordRequestForm
|
|
|
|
import json
|
|
from .config import settings
|
|
from .database import init_db, AsyncSessionLocal
|
|
from .core.pool import pool
|
|
from .core.epg_manager import epg_background_task
|
|
from .database import get_db
|
|
|
|
logging.basicConfig(
|
|
level=getattr(logging, settings.log_level),
|
|
format="%(asctime)s %(levelname)s %(name)s — %(message)s",
|
|
handlers=[logging.StreamHandler(sys.stdout)],
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def _auto_sync_loop() -> None:
|
|
"""Check every 30 minutes if any provider is due for auto-sync."""
|
|
from sqlalchemy import select
|
|
from .models.provider import ProviderAccount
|
|
from .api.admin.providers import _run_sync, _sync_status
|
|
|
|
await asyncio.sleep(60) # delay first check so startup completes
|
|
while True:
|
|
try:
|
|
async with AsyncSessionLocal() as db:
|
|
result = await db.execute(
|
|
select(ProviderAccount).where(ProviderAccount.auto_sync_hours > 0, ProviderAccount.is_active == True) # noqa: E712
|
|
)
|
|
providers = result.scalars().all()
|
|
now = asyncio.get_event_loop().time()
|
|
import time as _time
|
|
wall_now = _time.time()
|
|
for p in providers:
|
|
interval_secs = p.auto_sync_hours * 3600
|
|
last_sync_ts = p.last_sync_at.timestamp() if p.last_sync_at else 0
|
|
if wall_now - last_sync_ts >= interval_secs:
|
|
if _sync_status.get(p.id, {}).get("status") == "running":
|
|
continue
|
|
logger.info(f"Auto-sync triggered for provider {p.id} ({p.name})")
|
|
cats = json.loads(p.last_sync_categories) if p.last_sync_categories else {}
|
|
asyncio.create_task(_run_sync(
|
|
p.id, p.base_url, p.username, p.password,
|
|
selected_live=set(cats["live"]) if cats.get("live") is not None else None,
|
|
selected_movie=set(cats["movie"]) if cats.get("movie") is not None else None,
|
|
selected_series=set(cats["series"]) if cats.get("series") is not None else None,
|
|
))
|
|
except Exception as e:
|
|
logger.error(f"Auto-sync loop error: {e}")
|
|
await asyncio.sleep(1800) # check every 30 minutes
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
logger.info("KiraStream starting up...")
|
|
await init_db()
|
|
await pool.init()
|
|
epg_task = asyncio.create_task(epg_background_task(get_db))
|
|
auto_sync_task = asyncio.create_task(_auto_sync_loop())
|
|
logger.info("KiraStream ready")
|
|
yield
|
|
logger.info("KiraStream shutting down...")
|
|
await pool.close()
|
|
epg_task.cancel()
|
|
auto_sync_task.cancel()
|
|
for t in (epg_task, auto_sync_task):
|
|
try:
|
|
await t
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
|
|
app = FastAPI(
|
|
title="KiraStream",
|
|
description="Professional IPTV Management with Xtream Codes",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
docs_url="/api/docs",
|
|
redoc_url="/api/redoc",
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# --- Admin auth ---
|
|
from .api.auth import login, Token # noqa: E402
|
|
|
|
@app.post("/api/admin/auth/token", response_model=Token, tags=["admin"])
|
|
async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
|
|
return await login(form_data)
|
|
|
|
# --- Admin routers ---
|
|
from .api.admin.providers import router as providers_router # noqa: E402
|
|
from .api.admin.users import router as users_router # noqa: E402
|
|
from .api.admin.catalogs import router as catalogs_router # noqa: E402
|
|
from .api.admin.epg_admin import router as epg_admin_router # noqa: E402
|
|
from .api.admin.monitoring import router as monitoring_router # noqa: E402
|
|
from .api.admin.custom_categories import router as custom_cats_router # noqa: E402
|
|
from .api.admin.logs import router as logs_router # noqa: E402
|
|
from .api.admin.settings import router as settings_router # noqa: E402
|
|
from .api.admin.jellyfin_admin import router as jellyfin_router # noqa: E402
|
|
|
|
app.include_router(providers_router, prefix="/api/admin")
|
|
app.include_router(users_router, prefix="/api/admin")
|
|
app.include_router(catalogs_router, prefix="/api/admin")
|
|
app.include_router(epg_admin_router, prefix="/api/admin")
|
|
app.include_router(monitoring_router, prefix="/api/admin")
|
|
app.include_router(custom_cats_router, prefix="/api/admin")
|
|
app.include_router(logs_router, prefix="/api/admin")
|
|
app.include_router(settings_router, prefix="/api/admin")
|
|
app.include_router(jellyfin_router, prefix="/api/admin")
|
|
|
|
# --- Xtream Codes compatible API (public) ---
|
|
from .api.xtream import router as xtream_router # noqa: E402
|
|
app.include_router(xtream_router)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok", "service": "kirastream"}
|
|
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return JSONResponse({"message": "KiraStream IPTV Server", "docs": "/api/docs"})
|