Spaces:
Runtime error
Runtime error
| 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 | |
| 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"]) | |
| async def root(): | |
| return {"status": "ok", "org": ORG, "version": "3.0", "dev_mode": DEV_MODE, "accounts": list(ACCOUNTS.keys())} | |
| async def root_health(request: Request): | |
| return {"ok": True, "data": {"status": "healthy", "version": "3.0.0", "dev_mode": DEV_MODE}} | |
| async def compat_status(request: Request): | |
| from routers.archive import full_status | |
| return await full_status(request) | |
| async def compat_network(request: Request): | |
| from routers.archive import network | |
| return await network(request) | |
| 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) | |
| async def compat_archive_active(request: Request): | |
| from routers.archive import handle_archive_active | |
| return await handle_archive_active(request) | |
| async def compat_archive_stats(request: Request): | |
| from routers.archive import handle_stats | |
| return await handle_stats(request) | |
| async def compat_archive_storage(request: Request): | |
| from routers.archive import handle_storage | |
| return await handle_storage(request) | |
| async def compat_orchestrate(request: Request, target: str = "all", action: str = "ping"): | |
| from routers.archive import orchestrate | |
| return await orchestrate(request, target, action) | |
| 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) | |
| 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) | |
| 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) | |
| async def global_exception_handler(request, exc): | |
| return JSONResponse(status_code=500, content={"ok": False, "error": str(exc)[:300]}) | |