Teddpathhh commited on
Commit
08bf391
·
verified ·
1 Parent(s): 19773ce

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +24 -0
  2. app.py +230 -0
  3. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gunakan Python resmi
2
+ FROM python:3.11-slim
3
+
4
+ # Set workdir
5
+ WORKDIR /app
6
+
7
+ # Copy requirements
8
+ COPY requirements.txt /app/requirements.txt
9
+
10
+ # Install dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy semua source code
14
+ COPY . /app
15
+
16
+ # BERIKAN IZIN AKSES WRITE (Penting untuk Hugging Face)
17
+ # Kita buat folder 'output' duluan dan beri akses penuh agar aplikasi bisa menyimpan gambar
18
+ RUN mkdir -p /app/output && chmod -R 777 /app
19
+
20
+ # Expose port untuk Hugging Face
21
+ EXPOSE 7860
22
+
23
+ # Jalankan FastAPI pakai uvicorn
24
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.responses import FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from enkacard import encbanner
5
+ import asyncio
6
+ import os
7
+ import re
8
+ import time
9
+ from pathlib import Path
10
+ import httpx
11
+
12
+ app = FastAPI(title="Genshin Enka API")
13
+
14
+ OUTDIR = Path("output")
15
+ OUTDIR.mkdir(exist_ok=True)
16
+ app.mount("/static", StaticFiles(directory=OUTDIR), name="static")
17
+
18
+ # JSON cache memory
19
+ JSON_CACHE = {}
20
+ CACHE_EXPIRE = 300 # 5 menit
21
+
22
+ # Enka data cache
23
+ ENKA_CACHE = {}
24
+ ENKA_CACHE_EXPIRE = 300 # 5 menit
25
+
26
+ # Image cache expire
27
+ IMAGE_EXPIRE = 3600 # 1 jam
28
+
29
+
30
+ def safe_name(s: str):
31
+ return re.sub(r'[^A-Za-z0-9_.-]', '_', str(s))
32
+
33
+
34
+ # --- Cleanup old images ---
35
+ def cleanup_images():
36
+ now = time.time()
37
+ for f in OUTDIR.glob("*.png"):
38
+ if now - f.stat().st_mtime > IMAGE_EXPIRE:
39
+ try:
40
+ f.unlink()
41
+ except:
42
+ pass
43
+
44
+
45
+ # --- JSON cache helpers ---
46
+ def get_json_cache(uid):
47
+ if uid in JSON_CACHE:
48
+ data, ts = JSON_CACHE[uid]
49
+ if time.time() - ts < CACHE_EXPIRE:
50
+ return data
51
+ return None
52
+
53
+
54
+ def save_json_cache(uid, data):
55
+ JSON_CACHE[uid] = (data, time.time())
56
+
57
+
58
+ # --- Enka cache helpers ---
59
+ def get_enka_cache(uid):
60
+ if uid in ENKA_CACHE:
61
+ data, ts = ENKA_CACHE[uid]
62
+ if time.time() - ts < ENKA_CACHE_EXPIRE:
63
+ return data
64
+ return None
65
+
66
+
67
+ def save_enka_cache(uid, data):
68
+ ENKA_CACHE[uid] = (data, time.time())
69
+
70
+
71
+ # --- Fetch Enka API ---
72
+ async def fetch_enka(uid):
73
+ cached = get_enka_cache(uid)
74
+ if cached:
75
+ return cached
76
+
77
+ url = f"https://enka.network/api/uid/{uid}"
78
+ async with httpx.AsyncClient(timeout=20) as client:
79
+ r = await client.get(url)
80
+ if r.status_code != 200:
81
+ return None
82
+ data = r.json()
83
+
84
+ save_enka_cache(uid, data)
85
+ return data
86
+
87
+
88
+ # --- Update enka data background ---
89
+ async def background_update():
90
+ try:
91
+ print("Updating Enka data...")
92
+ await encbanner.update()
93
+ print("Update selesai")
94
+ except Exception as e:
95
+ print("Update gagal:", e)
96
+
97
+
98
+ @app.on_event("startup")
99
+ async def startup_event():
100
+ asyncio.create_task(background_update())
101
+
102
+
103
+ # --- Generate banner ---
104
+ async def make_banner(uid: str):
105
+ cleanup_images()
106
+
107
+ existing = list(OUTDIR.glob(f"banner_{uid}_*.png"))
108
+ if existing:
109
+ return existing
110
+
111
+ async with encbanner.ENC(uid=uid) as encard:
112
+ result = await encard.creat()
113
+ if not result or not getattr(result, "card", None):
114
+ return None
115
+
116
+ saved_files = []
117
+ for i, card in enumerate(result.card, start=1):
118
+ fname = OUTDIR / f"banner_{uid}_{i}_{safe_name(card.name)}.png"
119
+ card.card.save(fname)
120
+ saved_files.append(fname)
121
+
122
+ return saved_files
123
+
124
+
125
+ # --- Generate profile ---
126
+ async def make_profile(uid: str):
127
+ cleanup_images()
128
+
129
+ existing = list(OUTDIR.glob(f"profile_{uid}_*.png"))
130
+ if existing:
131
+ return existing[0]
132
+
133
+ try:
134
+ async with encbanner.ENC(uid=uid) as encard:
135
+ result = await encard.profile(card=True)
136
+
137
+ if not result or not getattr(result, "card", None):
138
+ raise Exception("Profile card missing")
139
+
140
+ fname = OUTDIR / f"profile_{uid}_{safe_name(result.player.name)}.png"
141
+ result.card.save(fname)
142
+ return fname
143
+
144
+ except Exception as e:
145
+ print("Avatar error fallback:", e)
146
+
147
+ # fallback traveller
148
+ fallback = OUTDIR / "traveller.png"
149
+ if fallback.exists():
150
+ return fallback
151
+ return None
152
+
153
+
154
+ @app.get("/")
155
+ async def root():
156
+ return {"status": "Citedd -- API aktif "}
157
+
158
+
159
+ # --- JSON API ---
160
+ @app.get("/api/genshin/{uid}")
161
+ async def genshin(uid: str):
162
+ cached = get_json_cache(uid)
163
+ if cached:
164
+ return cached
165
+
166
+ try:
167
+ banners = await make_banner(uid)
168
+ profile = await make_profile(uid)
169
+ enka_data = await fetch_enka(uid)
170
+
171
+ if not banners:
172
+ raise HTTPException(404, "UID tidak ditemukan / private")
173
+
174
+ # default values
175
+ nickname = None
176
+ ar = None
177
+ abyss = None
178
+
179
+ if enka_data:
180
+ p = enka_data.get("playerInfo", {})
181
+ nickname = p.get("nickname")
182
+ ar = p.get("level")
183
+
184
+ floor = p.get("towerFloorIndex")
185
+ level = p.get("towerLevelIndex")
186
+ if floor and level:
187
+ abyss = f"{floor}-{level}"
188
+
189
+ result = {
190
+ "uid": uid,
191
+ "nickname": nickname,
192
+ "adventure_rank": ar,
193
+ "spiral_abyss": abyss,
194
+ "profile_card": f"/static/{profile.name}" if profile else None,
195
+ "character_cards": [f"/static/{b.name}" for b in banners]
196
+ }
197
+
198
+ save_json_cache(uid, result)
199
+ return result
200
+
201
+ except Exception as e:
202
+ raise HTTPException(500, str(e))
203
+
204
+
205
+ # --- Direct image endpoints ---
206
+ @app.get("/banner")
207
+ async def banner(uid: str):
208
+ files = await make_banner(uid)
209
+ if not files:
210
+ raise HTTPException(404, "Banner tidak ada")
211
+ return FileResponse(files[0])
212
+
213
+
214
+ @app.get("/profile")
215
+ async def profile(uid: str):
216
+ fname = await make_profile(uid)
217
+ if not fname:
218
+ raise HTTPException(404, "Profile tidak ada")
219
+ return FileResponse(fname)
220
+
221
+
222
+ if __name__ == "__main__":
223
+ import uvicorn
224
+
225
+ uvicorn.run(
226
+ "app:app",
227
+ host="0.0.0.0",
228
+ port=int(os.environ.get("SERVER_PORT", 5000)),
229
+ reload=False
230
+ )
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ enkacard
4
+ pillow
5
+ httpx
6
+ aiofiles