Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,67 +1,69 @@
|
|
1 |
|
2 |
-
|
3 |
-
import os, time, json, datetime, requests, gradio as gr
|
4 |
|
5 |
-
# 1. Vercel API
|
6 |
-
TOKEN = os.getenv("SVR_TOKEN")
|
7 |
-
TEAM = os.getenv("VERCEL_TEAM_ID")
|
8 |
if not TOKEN:
|
9 |
raise EnvironmentError("SVR_TOKEN ํ๊ฒฝ๋ณ์๋ฅผ ์ค์ ํ์ธ์.")
|
10 |
API = "https://api.vercel.com"
|
11 |
HEAD = {"Authorization": f"Bearer {TOKEN}"}
|
12 |
|
13 |
-
# 2. BEST
|
14 |
-
BEST_FILE
|
15 |
-
PER_PAGE = 48
|
16 |
def _init_best():
|
17 |
if not os.path.exists(BEST_FILE):
|
18 |
json.dump([], open(BEST_FILE, "w"))
|
19 |
def _load_best():
|
20 |
try:
|
21 |
-
|
|
|
|
|
|
|
|
|
22 |
except Exception:
|
23 |
return []
|
24 |
|
25 |
-
# 3.
|
|
|
|
|
26 |
def fetch_all(limit=200):
|
|
|
27 |
try:
|
28 |
params = {"limit": limit}
|
29 |
-
if TEAM:
|
30 |
-
|
31 |
-
r = requests.get(f"{API}/v6/deployments",
|
32 |
-
headers=HEAD, params=params, timeout=30)
|
33 |
r.raise_for_status()
|
34 |
|
35 |
-
out
|
36 |
for d in r.json().get("deployments", []):
|
37 |
if d.get("state") != "READY":
|
38 |
continue
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
42 |
out.append({
|
43 |
"title": d.get("name", "(์ ๋ชฉ ์์)"),
|
44 |
"url" : url_full,
|
45 |
-
"ts" : int(
|
46 |
})
|
47 |
return sorted(out, key=lambda x: x["ts"], reverse=True)
|
48 |
except Exception as e:
|
49 |
print("Vercel API ์ค๋ฅ:", e)
|
50 |
return []
|
51 |
|
52 |
-
# 4. ํ์ด์ง๋ค์ด์
|
53 |
def page(lst, pg):
|
54 |
-
s
|
55 |
-
|
56 |
-
total = (len(lst)+PER_PAGE-1)//PER_PAGE
|
57 |
return lst[s:e], total
|
58 |
|
59 |
-
# 5. HTML
|
60 |
def html(cards, pg, total):
|
61 |
if not cards:
|
62 |
return "<div style='text-align:center;padding:70px;color:#555;'>ํ์ํ ๋ฐฐํฌ๊ฐ ์์ต๋๋ค.</div>"
|
63 |
-
|
64 |
-
css = r"""
|
65 |
<style>
|
66 |
body{margin:0;font-family:Poppins,sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
|
67 |
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px 24px;margin:0 20px 60px;}
|
@@ -78,27 +80,25 @@ def html(cards, pg, total):
|
|
78 |
.foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
|
79 |
.link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
|
80 |
.cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;}
|
81 |
-
</style>
|
82 |
-
""
|
83 |
-
h = css + "<div class='grid'>"
|
84 |
for c in cards:
|
85 |
-
date
|
86 |
-
h
|
87 |
<div class='card'>
|
88 |
<div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
|
89 |
<div class='frame'><iframe src="{c['url']}" loading="lazy"
|
90 |
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
|
91 |
<div class='foot'><a class='link' href="{c['url']}" target="_blank">์๋ณธโ</a></div>
|
92 |
</div>"""
|
93 |
-
h
|
94 |
return h
|
95 |
|
96 |
-
# 6. Gradio UI
|
97 |
def build():
|
98 |
_init_best()
|
99 |
with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
|
100 |
gr.Markdown("<h1 style='text-align:center;padding:32px 0 0;color:#333;'>๐ฎ Vibe Game Craft</h1>")
|
101 |
-
|
102 |
with gr.Row():
|
103 |
b_new = gr.Button("NEW", size="sm")
|
104 |
b_best = gr.Button("BEST", size="sm")
|
@@ -106,48 +106,31 @@ def build():
|
|
106 |
b_next = gr.Button("Next โก๏ธ", size="sm")
|
107 |
b_ref = gr.Button("๐ Reload", size="sm")
|
108 |
|
109 |
-
tab = gr.State("new")
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
def show_best(p=1):
|
119 |
-
data, tot = page(_load_best(), p)
|
120 |
-
return html(data, p, tot), "best", p
|
121 |
-
|
122 |
-
# Prev / Next
|
123 |
-
def prev(t, n, b):
|
124 |
-
if t=="new":
|
125 |
-
n=max(1,n-1); h,_,_ = show_new(n); return h, n, b
|
126 |
-
b=max(1,b-1); h,_,_ = show_best(b); return h, n, b
|
127 |
-
def nxt(t, n, b):
|
128 |
if t=="new":
|
129 |
maxp=(len(fetch_all())+PER_PAGE-1)//PER_PAGE
|
130 |
-
n=min(maxp,n+1); h,_,_
|
131 |
maxp=(len(_load_best())+PER_PAGE-1)//PER_PAGE
|
132 |
-
b=min(maxp,b+1); h,_,_
|
133 |
-
|
134 |
-
# Reload
|
135 |
-
def reload(t, n, b):
|
136 |
-
return show_new(n)[0] if t=="new" else show_best(b)[0]
|
137 |
-
|
138 |
-
# ๋ฒํผ ์ฐ๊ฒฐ
|
139 |
-
b_new.click(show_new, outputs=[out, tab, np])
|
140 |
-
b_best.click(show_best, outputs=[out, tab, bp])
|
141 |
-
b_prev.click(prev, inputs=[tab, np, bp], outputs=[out, np, bp])
|
142 |
-
b_next.click(nxt, inputs=[tab, np, bp], outputs=[out, np, bp])
|
143 |
-
b_ref.click(reload, inputs=[tab, np, bp], outputs=[out])
|
144 |
|
145 |
-
|
146 |
-
|
|
|
|
|
|
|
|
|
147 |
|
|
|
148 |
return demo
|
149 |
|
150 |
-
# 7. ์คํ
|
151 |
app = build()
|
152 |
|
153 |
if __name__ == "__main__":
|
|
|
1 |
|
2 |
+
import os, re, time, json, datetime, requests, gradio as gr
|
|
|
3 |
|
4 |
+
# โโโโโโโโโโโโโโโโโโโโโ 1. Vercel API โโโโโโโโโโโโโโโโโโโโโ
|
5 |
+
TOKEN = os.getenv("SVR_TOKEN")
|
6 |
+
TEAM = os.getenv("VERCEL_TEAM_ID")
|
7 |
if not TOKEN:
|
8 |
raise EnvironmentError("SVR_TOKEN ํ๊ฒฝ๋ณ์๋ฅผ ์ค์ ํ์ธ์.")
|
9 |
API = "https://api.vercel.com"
|
10 |
HEAD = {"Authorization": f"Bearer {TOKEN}"}
|
11 |
|
12 |
+
# โโโโโโโโโโโโโโโโโโโโโ 2. BEST ๋ฐ์ดํฐ โโโโโโโโโโโโโโโโโโโโ
|
13 |
+
BEST_FILE, PER_PAGE = "best_games.json", 48
|
|
|
14 |
def _init_best():
|
15 |
if not os.path.exists(BEST_FILE):
|
16 |
json.dump([], open(BEST_FILE, "w"))
|
17 |
def _load_best():
|
18 |
try:
|
19 |
+
data = json.load(open(BEST_FILE))
|
20 |
+
for it in data:
|
21 |
+
if "ts" not in it:
|
22 |
+
it["ts"] = int(it.get("timestamp", time.time()))
|
23 |
+
return data
|
24 |
except Exception:
|
25 |
return []
|
26 |
|
27 |
+
# โโโโโโโโโโโโโโโโโโโโโ 3. NEW: 6๊ธ์ vercel.app ๋ฐฐํฌ โโโโโ
|
28 |
+
PAT = re.compile(r"^[a-z0-9]{6}\.vercel\.app$", re.I)
|
29 |
+
|
30 |
def fetch_all(limit=200):
|
31 |
+
"""v6/deployments โ READY ์ํ ์ค url์ด 6๊ธ์ vercel.app ์ธ ๊ฒ๋ง"""
|
32 |
try:
|
33 |
params = {"limit": limit}
|
34 |
+
if TEAM: params["teamId"] = TEAM
|
35 |
+
r = requests.get(f"{API}/v6/deployments", headers=HEAD, params=params, timeout=30)
|
|
|
|
|
36 |
r.raise_for_status()
|
37 |
|
38 |
+
out=[]
|
39 |
for d in r.json().get("deployments", []):
|
40 |
if d.get("state") != "READY":
|
41 |
continue
|
42 |
+
host = d.get("url", "")
|
43 |
+
if not PAT.match(host):
|
44 |
+
continue # 6๊ธ์ vercel.app ๊ท์น ๋ถ์ผ์น โ ์ ์ธ
|
45 |
+
url_full = f"https://{host}"
|
46 |
out.append({
|
47 |
"title": d.get("name", "(์ ๋ชฉ ์์)"),
|
48 |
"url" : url_full,
|
49 |
+
"ts" : int(d.get("created", time.time()*1000) / 1000)
|
50 |
})
|
51 |
return sorted(out, key=lambda x: x["ts"], reverse=True)
|
52 |
except Exception as e:
|
53 |
print("Vercel API ์ค๋ฅ:", e)
|
54 |
return []
|
55 |
|
56 |
+
# โโโโโโโโโโโโโโโโโโโโโ 4. ํ์ด์ง๋ค์ด์
โโโโโโโโโโโโโโโโโโโ
|
57 |
def page(lst, pg):
|
58 |
+
s=(pg-1)*PER_PAGE; e=s+PER_PAGE
|
59 |
+
total=(len(lst)+PER_PAGE-1)//PER_PAGE
|
|
|
60 |
return lst[s:e], total
|
61 |
|
62 |
+
# โโโโโโโโโโโโโโโโโโโโโ 5. HTML 3-์ด ๊ทธ๋ฆฌ๋ โโโโโโโโโโโโโโโ
|
63 |
def html(cards, pg, total):
|
64 |
if not cards:
|
65 |
return "<div style='text-align:center;padding:70px;color:#555;'>ํ์ํ ๋ฐฐํฌ๊ฐ ์์ต๋๋ค.</div>"
|
66 |
+
css=r"""
|
|
|
67 |
<style>
|
68 |
body{margin:0;font-family:Poppins,sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
|
69 |
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px 24px;margin:0 20px 60px;}
|
|
|
80 |
.foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
|
81 |
.link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
|
82 |
.cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;}
|
83 |
+
</style>"""
|
84 |
+
h=css+"<div class='grid'>"
|
|
|
85 |
for c in cards:
|
86 |
+
date=datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d")
|
87 |
+
h+=f"""
|
88 |
<div class='card'>
|
89 |
<div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
|
90 |
<div class='frame'><iframe src="{c['url']}" loading="lazy"
|
91 |
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
|
92 |
<div class='foot'><a class='link' href="{c['url']}" target="_blank">์๋ณธโ</a></div>
|
93 |
</div>"""
|
94 |
+
h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
|
95 |
return h
|
96 |
|
97 |
+
# โโโโโโโโโโโโโโโโโโโโโ 6. Gradio Blocks UI โโโโโโโโโโโโโโโ
|
98 |
def build():
|
99 |
_init_best()
|
100 |
with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
|
101 |
gr.Markdown("<h1 style='text-align:center;padding:32px 0 0;color:#333;'>๐ฎ Vibe Game Craft</h1>")
|
|
|
102 |
with gr.Row():
|
103 |
b_new = gr.Button("NEW", size="sm")
|
104 |
b_best = gr.Button("BEST", size="sm")
|
|
|
106 |
b_next = gr.Button("Next โก๏ธ", size="sm")
|
107 |
b_ref = gr.Button("๐ Reload", size="sm")
|
108 |
|
109 |
+
tab = gr.State("new"); np = gr.State(1); bp = gr.State(1); out = gr.HTML()
|
110 |
+
|
111 |
+
def show_new(p=1): d,t=page(fetch_all(),p); return html(d,p,t),"new",p
|
112 |
+
def show_best(p=1): d,t=page(_load_best(),p); return html(d,p,t),"best",p
|
113 |
+
|
114 |
+
def prev(t,n,b):
|
115 |
+
if t=="new": n=max(1,n-1); h,_,_=show_new(n); return h,n,b
|
116 |
+
b=max(1,b-1); h,_,_=show_best(b); return h,n,b
|
117 |
+
def nxt(t,n,b):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
if t=="new":
|
119 |
maxp=(len(fetch_all())+PER_PAGE-1)//PER_PAGE
|
120 |
+
n=min(maxp,n+1); h,_,_=show_new(n); return h,n,b
|
121 |
maxp=(len(_load_best())+PER_PAGE-1)//PER_PAGE
|
122 |
+
b=min(maxp,b+1); h,_,_=show_best(b); return h,n,b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
+
b_new.click(show_new, outputs=[out,tab,np])
|
125 |
+
b_best.click(show_best,outputs=[out,tab,bp])
|
126 |
+
b_prev.click(prev, inputs=[tab,np,bp], outputs=[out,np,bp])
|
127 |
+
b_next.click(nxt, inputs=[tab,np,bp], outputs=[out,np,bp])
|
128 |
+
b_ref.click(lambda t,n,b: show_new(n)[0] if t=="new" else show_best(b)[0],
|
129 |
+
inputs=[tab,np,bp], outputs=[out])
|
130 |
|
131 |
+
demo.load(show_new, outputs=[out,tab,np])
|
132 |
return demo
|
133 |
|
|
|
134 |
app = build()
|
135 |
|
136 |
if __name__ == "__main__":
|