Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException | |
| from fastapi.responses import FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from enkacard import encbanner | |
| import asyncio | |
| import os | |
| import re | |
| import time | |
| from pathlib import Path | |
| import httpx | |
| app = FastAPI(title="Genshin Enka API") | |
| OUTDIR = Path("output") | |
| OUTDIR.mkdir(exist_ok=True) | |
| app.mount("/static", StaticFiles(directory=OUTDIR), name="static") | |
| # JSON cache memory | |
| JSON_CACHE = {} | |
| CACHE_EXPIRE = 300 # 5 menit | |
| # Enka data cache | |
| ENKA_CACHE = {} | |
| ENKA_CACHE_EXPIRE = 300 # 5 menit | |
| # Image cache expire | |
| IMAGE_EXPIRE = 3600 # 1 jam | |
| def safe_name(s: str): | |
| return re.sub(r'[^A-Za-z0-9_.-]', '_', str(s)) | |
| # --- Cleanup old images --- | |
| def cleanup_images(): | |
| now = time.time() | |
| for f in OUTDIR.glob("*.png"): | |
| if now - f.stat().st_mtime > IMAGE_EXPIRE: | |
| try: | |
| f.unlink() | |
| except: | |
| pass | |
| # --- JSON cache helpers --- | |
| def get_json_cache(uid): | |
| if uid in JSON_CACHE: | |
| data, ts = JSON_CACHE[uid] | |
| if time.time() - ts < CACHE_EXPIRE: | |
| return data | |
| return None | |
| def save_json_cache(uid, data): | |
| JSON_CACHE[uid] = (data, time.time()) | |
| # --- Enka cache helpers --- | |
| def get_enka_cache(uid): | |
| if uid in ENKA_CACHE: | |
| data, ts = ENKA_CACHE[uid] | |
| if time.time() - ts < ENKA_CACHE_EXPIRE: | |
| return data | |
| return None | |
| def save_enka_cache(uid, data): | |
| ENKA_CACHE[uid] = (data, time.time()) | |
| # --- Fetch Enka API --- | |
| async def fetch_enka(uid): | |
| cached = get_enka_cache(uid) | |
| if cached: | |
| return cached | |
| url = f"https://enka.network/api/uid/{uid}" | |
| async with httpx.AsyncClient(timeout=20) as client: | |
| r = await client.get(url) | |
| if r.status_code != 200: | |
| return None | |
| data = r.json() | |
| save_enka_cache(uid, data) | |
| return data | |
| # --- Update enka data background --- | |
| async def background_update(): | |
| try: | |
| print("Updating Enka data...") | |
| await encbanner.update() | |
| print("Update selesai") | |
| except Exception as e: | |
| print("Update gagal:", e) | |
| async def startup_event(): | |
| asyncio.create_task(background_update()) | |
| # --- Generate banner --- | |
| async def make_banner(uid: str): | |
| cleanup_images() | |
| existing = list(OUTDIR.glob(f"banner_{uid}_*.png")) | |
| if existing: | |
| return existing | |
| async with encbanner.ENC(uid=uid) as encard: | |
| result = await encard.creat() | |
| if not result or not getattr(result, "card", None): | |
| return None | |
| saved_files = [] | |
| for i, card in enumerate(result.card, start=1): | |
| fname = OUTDIR / f"banner_{uid}_{i}_{safe_name(card.name)}.png" | |
| card.card.save(fname) | |
| saved_files.append(fname) | |
| return saved_files | |
| # --- Generate profile --- | |
| async def make_profile(uid: str): | |
| cleanup_images() | |
| existing = list(OUTDIR.glob(f"profile_{uid}_*.png")) | |
| if existing: | |
| return existing[0] | |
| try: | |
| async with encbanner.ENC(uid=uid) as encard: | |
| result = await encard.profile(card=True) | |
| if not result or not getattr(result, "card", None): | |
| raise Exception("Profile card missing") | |
| fname = OUTDIR / f"profile_{uid}_{safe_name(result.player.name)}.png" | |
| result.card.save(fname) | |
| return fname | |
| except Exception as e: | |
| print("Avatar error fallback:", e) | |
| # fallback traveller | |
| fallback = OUTDIR / "traveller.png" | |
| if fallback.exists(): | |
| return fallback | |
| return None | |
| async def root(): | |
| return {"status": "Citedd -- API aktif "} | |
| # --- JSON API --- | |
| async def genshin(uid: str): | |
| cached = get_json_cache(uid) | |
| if cached: | |
| return cached | |
| try: | |
| banners = await make_banner(uid) | |
| profile = await make_profile(uid) | |
| enka_data = await fetch_enka(uid) | |
| if not banners: | |
| raise HTTPException(404, "UID tidak ditemukan / private") | |
| # default values | |
| nickname = None | |
| ar = None | |
| abyss = None | |
| if enka_data: | |
| p = enka_data.get("playerInfo", {}) | |
| nickname = p.get("nickname") | |
| ar = p.get("level") | |
| floor = p.get("towerFloorIndex") | |
| level = p.get("towerLevelIndex") | |
| if floor and level: | |
| abyss = f"{floor}-{level}" | |
| result = { | |
| "uid": uid, | |
| "nickname": nickname, | |
| "adventure_rank": ar, | |
| "spiral_abyss": abyss, | |
| "profile_card": f"/static/{profile.name}" if profile else None, | |
| "character_cards": [f"/static/{b.name}" for b in banners] | |
| } | |
| save_json_cache(uid, result) | |
| return result | |
| except Exception as e: | |
| raise HTTPException(500, str(e)) | |
| # --- Direct image endpoints --- | |
| async def banner(uid: str): | |
| files = await make_banner(uid) | |
| if not files: | |
| raise HTTPException(404, "Banner tidak ada") | |
| return FileResponse(files[0]) | |
| async def profile(uid: str): | |
| fname = await make_profile(uid) | |
| if not fname: | |
| raise HTTPException(404, "Profile tidak ada") | |
| return FileResponse(fname) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run( | |
| "app:app", | |
| host="0.0.0.0", | |
| port=int(os.environ.get("SERVER_PORT", 5000)), | |
| reload=False | |
| ) |