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) @app.on_event("startup") 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 @app.get("/") async def root(): return {"status": "Citedd -- API aktif "} # --- JSON API --- @app.get("/api/genshin/{uid}") 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 --- @app.get("/banner") async def banner(uid: str): files = await make_banner(uid) if not files: raise HTTPException(404, "Banner tidak ada") return FileResponse(files[0]) @app.get("/profile") 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 )