api / app.py
PipelineFix
fix: add missing HTTPException import to app.py compat layer
7ff3fe2
Raw
History Blame
9.1 kB
import json
import time
import asyncio
import aiohttp
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 season_number INTEGER",
"ALTER TABLE archives ADD COLUMN episode_number INTEGER",
"ALTER TABLE archives ADD COLUMN source TEXT",
"ALTER TABLE archives ADD COLUMN audio TEXT",
"ALTER TABLE archives ADD COLUMN network TEXT",
"ALTER TABLE archives ADD COLUMN highlights TEXT",
"ALTER TABLE archives ADD COLUMN full_metadata TEXT",
"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
_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])
response = JSONResponse(
content=json.loads(body),
status_code=response.status_code,
headers=dict(response.headers),
)
parsed = json.loads(body)
_INMEM_CACHE[path] = parsed
_INMEM_TTL[path] = now + cached_ttl
except:
pass
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:
r = await init_tables(INIT_SQLS)
i = await init_tables(INDEX_SQLS)
f = await init_tables(FTS_SQLS)
s = await init_tables(SEED_SQLS)
for migration in _MIGRATIONS:
try:
await exec(migration)
except Exception:
pass
try:
await exec("UPDATE archives SET status='archived' WHERE status IS NULL OR status=''")
except Exception:
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 Unified API", version="3.0.0", lifespan=lifespan)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=not DEV_MODE,
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.get("/")
async def root():
return {"status": "ok", "org": ORG, "version": "3.0", "dev_mode": DEV_MODE, "accounts": list(ACCOUNTS.keys())}
@app.get("/health")
@limiter.limit("30/minute")
async def root_health(request: Request):
return {"ok": True, "data": {"status": "healthy", "version": "3.0.0", "dev_mode": DEV_MODE}}
@app.get("/status")
async def compat_status(request: Request):
from routers.archive import full_status
return await full_status(request)
@app.get("/network")
async def compat_network(request: Request):
from routers.archive import network
return await network(request)
@app.get("/archive/progress")
async def compat_archive_progress(request: Request, archive_id: str = None):
from routers.archive import handle_progress_get
return await handle_progress_get(request, archive_id)
@app.get("/archive/active")
async def compat_archive_active(request: Request):
from routers.archive import handle_archive_active
return await handle_archive_active(request)
@app.get("/archive/stats")
async def compat_archive_stats(request: Request):
from routers.archive import handle_stats
return await handle_stats(request)
@app.get("/archive/storage")
async def compat_archive_storage(request: Request):
from routers.archive import handle_storage
return await handle_storage(request)
@app.get("/orchestrate")
async def compat_orchestrate(request: Request, target: str = "all", action: str = "ping"):
from routers.archive import orchestrate
return await orchestrate(request, target, action)
@app.post("/archive/file")
async def compat_archive_file(request: Request):
from routers.archive import handle_archive_file, ArchiveFileRequest
body = await request.json()
req = ArchiveFileRequest(**body)
return await handle_archive_file(req, request)
@app.post("/archive/progress/update")
async def compat_archive_progress_update(request: Request):
from routers.archive import handle_progress_update, ProgressUpdateRequest
raw = await request.json()
aid = raw.get("archive_id", "")
if not aid:
raise HTTPException(status_code=400, detail="Missing archive_id")
req = ProgressUpdateRequest(archive_id=aid)
return await handle_progress_update(req, request)
@app.post("/archive/check-duplicate")
async def compat_archive_check_dup(request: Request):
from routers.archive import handle_check_duplicate, CheckDuplicateRequest
body = await request.json()
req = CheckDuplicateRequest(**body)
return await handle_check_duplicate(req, request)
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
return JSONResponse(status_code=500, content={"ok": False, "error": str(exc)[:300]})