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