Files
KiraTV/backend/main.py
T
joaquin f5d6745ed0 Initial commit: KiraTV IPTV proxy server
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.
2026-05-17 23:22:05 +02:00

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"})