api / app.py
jamelloverz-sketch
PopCorn API v3.5 - Media metadata, people, images, open CORS
c18e004
Raw
History Blame Contribute Delete
6.65 kB
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]})