import json, time, asyncio, aiohttp, os from contextlib import asynccontextmanager from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from config import ORG, ACCOUNTS, CORS_ORIGINS, DEV_MODE from database import init_tables, exec, close_session from db_init import INIT_SQLS, INDEX_SQLS, FTS_SQLS, SEED_SQLS from ratelimit import limiter _KEEP_WARM_URLS = [ "https://mml-analysing-shyper-and-api-mid-structer-db.hf.space/health", "https://multimedia-cryptography-benchmarks-proxy.hf.space/health", "https://rayig-proxy.hf.space/health", "https://ToolKit-backend-proxy.hf.space/health", "https://Dgntsnfangskgsjafjyjysjtsjtsjwtjfsjyejyrky-proxy.hf.space/health", ] async def keep_warm(): while True: await asyncio.sleep(240) async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as sess: for url in _KEEP_WARM_URLS: try: async with sess.get(url) as resp: if resp.status == 200: await resp.read() except: pass _MIGRATIONS = [ "ALTER TABLE archives ADD COLUMN media_id INTEGER", "ALTER TABLE archives ADD COLUMN media_type TEXT", "ALTER TABLE archives ADD COLUMN file_name TEXT", "ALTER TABLE archives ADD COLUMN dataset_name TEXT", "ALTER TABLE archives ADD COLUMN account_key TEXT", "ALTER TABLE archives ADD COLUMN status TEXT DEFAULT 'archived'", "ALTER TABLE archives ADD COLUMN storage_channel_id INTEGER", "ALTER TABLE archives ADD COLUMN storage_msg_id INTEGER", ] from routers import health, movies, series, search, stream from routers import auth_r, user, social, chat, parties from routers import store, payments from routers import notifications, achievements, leaderboard, comments from routers import archive, websocket as ws_router from routers import social_feed, clubs, gamification, analytics, media as media_router _CACHEABLE_PATHS = ( "/api/v1/health", "/api/v1/movies/trending", "/api/v1/movies/featured", "/api/v1/movies/latest", "/api/v1/movies/genres", "/api/v1/stream/proxy/status", ) _INMEM_CACHE, _INMEM_TTL = {}, {} async def cache_middleware(request: Request, call_next): path = request.url.path if request.method != "GET" or path == "/favicon.ico": return await call_next(request) now = time.time() cached = _INMEM_CACHE.get(path) if cached and now < _INMEM_TTL.get(path, 0): return JSONResponse(content=cached, headers={"Cache-Control": "public, max-age=120","X-Cache":"HIT","Access-Control-Allow-Origin":"*"}) response = await call_next(request) if response.status_code != 200: return response cached_ttl = 30 for prefix in _CACHEABLE_PATHS: if path.startswith(prefix): cached_ttl = 120; break try: body = b"".join([chunk async for chunk in response.body_iterator]) parsed = json.loads(body) new_headers = dict(response.headers) new_headers["Cache-Control"] = f"public, max-age={cached_ttl}" new_resp = JSONResponse(content=parsed, status_code=response.status_code, headers=new_headers) _INMEM_CACHE[path] = parsed _INMEM_TTL[path] = now + cached_ttl return new_resp except: response.headers["Cache-Control"] = f"public, max-age={cached_ttl}" return response @asynccontextmanager async def lifespan(app: FastAPI): task = asyncio.create_task(keep_warm()) try: for sqls in [INIT_SQLS, INDEX_SQLS, FTS_SQLS, SEED_SQLS]: await init_tables(sqls) for migration in _MIGRATIONS: try: await exec(migration) except: pass try: await exec("UPDATE archives SET status='archived' WHERE status IS NULL OR status=''") except: pass except Exception as e: print(f"[init] error: {e}") yield task.cancel() try: await task except: pass await close_session() app = FastAPI(title="PopCorn API", version="3.5", lifespan=lifespan) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"]) app.middleware("http")(cache_middleware) # API Routes app.include_router(health.router, prefix="/api/v1", tags=["health"]) app.include_router(auth_r.router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(movies.router, prefix="/api/v1/movies", tags=["movies"]) app.include_router(series.router, prefix="/api/v1/series", tags=["series"]) app.include_router(search.router, prefix="/api/v1/search", tags=["search"]) app.include_router(stream.router, prefix="/api/v1/stream", tags=["stream"]) app.include_router(user.router, prefix="/api/v1/user", tags=["user"]) app.include_router(social.router, prefix="/api/v1/social", tags=["social"]) app.include_router(chat.router, prefix="/api/v1/chat", tags=["chat"]) app.include_router(parties.router, prefix="/api/v1/party", tags=["parties"]) app.include_router(store.router, prefix="/api/v1/store", tags=["store"]) app.include_router(payments.router, prefix="/api/v1/payments", tags=["payments"]) app.include_router(notifications.router, prefix="/api/v1/notifications", tags=["notifications"]) app.include_router(achievements.router, prefix="/api/v1/achievements", tags=["achievements"]) app.include_router(leaderboard.router, prefix="/api/v1/leaderboard", tags=["leaderboard"]) app.include_router(comments.router, tags=["comments"]) app.include_router(archive.router, prefix="/api/v1/archive", tags=["archive"]) app.include_router(ws_router.router, prefix="/api/v1/ws", tags=["websocket"]) app.include_router(social_feed.router, prefix="/api/v1/social", tags=["social-feed"]) app.include_router(clubs.router, prefix="/api/v1", tags=["clubs"]) app.include_router(gamification.router, prefix="/api/v1/game", tags=["gamification"]) app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"]) app.include_router(media_router.router, prefix="/api/v1/media", tags=["media"]) @app.get("/") async def root(): return {"status":"ok","org":ORG,"version":"3.5"} @app.get("/health") @limiter.limit("30/minute") async def root_health(request: Request): return {"ok":True,"data":{"status":"healthy","version":"3.5"}} @app.exception_handler(Exception) async def global_exception_handler(request, exc): return JSONResponse(status_code=500, content={"ok":False,"error":str(exc)[:300]})