Game-Gallery / app.py
ginipick's picture
Update app.py
3573333 verified
raw
history blame
10.3 kB
import os, time, json, requests, datetime, gradio as gr
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1. Vercel API ์„ค์ • โ”€ ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ํ† ํฐ ์ฝ๊ธฐ
VERCEL_API_TOKEN = os.getenv("SVR_TOKEN")
if not VERCEL_API_TOKEN:
raise EnvironmentError("ํ™˜๊ฒฝ ๋ณ€์ˆ˜ 'SVR_TOKEN'์ด ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค!")
VERCEL_API_URL = "https://api.vercel.com/v9"
HEADERS = {
"Authorization": f"Bearer {VERCEL_API_TOKEN}",
"Content-Type": "application/json",
}
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. ๊ฐค๋Ÿฌ๋ฆฌยทํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ธฐ๋ณธ๊ฐ’
BEST_GAMES_FILE = "best_games.json"
GAMES_PER_PAGE = 48
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. BEST ํƒญ์šฉ ์ƒ˜ํ”Œ ๊ฒŒ์ž„ ์ดˆ๊ธฐํ™”
def initialize_best_games():
if not os.path.exists(BEST_GAMES_FILE):
sample = [
{
"title": "ํ…ŒํŠธ๋ฆฌ์Šค",
"description": "ํด๋ž˜์‹ ํ…ŒํŠธ๋ฆฌ์Šค ๊ฒŒ์ž„",
"url": "https://tmkdop.vercel.app",
"timestamp": time.time(),
},
{
"title": "์Šค๋„ค์ดํฌ",
"description": "์ „ํ†ต์ ์ธ ์Šค๋„ค์ดํฌ ๊ฒŒ์ž„",
"url": "https://tmkdop.vercel.app",
"timestamp": time.time(),
},
{
"title": "ํŒฉ๋งจ",
"description": "๊ณ ์ „ ์•„์ผ€์ด๋“œ ๊ฒŒ์ž„",
"url": "https://tmkdop.vercel.app",
"timestamp": time.time(),
},
]
json.dump(sample, open(BEST_GAMES_FILE, "w"))
def load_best_games():
try:
return json.load(open(BEST_GAMES_FILE))
except Exception:
return []
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 4. ์ตœ์‹  Vercel ๋ฐฐํฌ โ†’ ๊ฒŒ์ž„ ๋ชฉ๋ก
def get_latest_deployments():
try:
r = requests.get(
f"{VERCEL_API_URL}/deployments",
headers=HEADERS,
params={"limit": 100},
timeout=30,
)
r.raise_for_status()
games = []
for d in r.json().get("deployments", []):
if d.get("state") != "READY":
continue
ts = (
int(d["createdAt"] / 1000)
if isinstance(d["createdAt"], (int, float))
else int(time.time())
)
games.append(
{
"title": d.get("name", "๊ฒŒ์ž„"),
"description": f"๋ฐฐํฌ๋œ ๊ฒŒ์ž„: {d.get('name')}",
"url": f"https://{d.get('url')}",
"timestamp": ts,
}
)
return sorted(games, key=lambda x: x["timestamp"], reverse=True)
except Exception as e:
print("Vercel API ์˜ค๋ฅ˜:", e)
return []
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 5. ํŽ˜์ด์ง€๋„ค์ด์…˜ ํ—ฌํผ
def paginate(lst, page):
start = (page - 1) * GAMES_PER_PAGE
end = start + GAMES_PER_PAGE
total = (len(lst) + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
return lst[start:end], total
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 6. ๊ฐค๋Ÿฌ๋ฆฌ HTML (์‚ฌ์ดํŠธ ๋ฏธ๋Ÿฌ๋ง ์Šคํƒ€์ผ)
def generate_gallery_html(games, page, total_pages, tab_name):
if not games:
return (
"<div style='text-align:center;padding:60px;'>ํ‘œ์‹œํ•  ๊ฒŒ์ž„์ด ์—†์Šต๋‹ˆ๋‹ค.</div>"
)
#โ€ŠCSS (ํ•„์š” ์ตœ์†Œ๋งŒ ์ธ๋ผ์ธ - reference ์Šคํƒ€์ผ ์žฌ๊ตฌ์„ฑ)
css = """
<style>
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:24px;margin-bottom:40px}
.item{background:white;border-radius:14px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,.1);transition:.3s}
.item:hover{transform:translateY(-6px);box-shadow:0 12px 30px rgba(0,0,0,.15)}
.hdr{padding:14px 18px;background:rgba(255,255,255,.8);backdrop-filter:blur(6px);border-bottom:1px solid #eee}
.ttl{font-size:1.1rem;font-weight:700;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.meta{font-size:.85rem;color:#666;margin-top:4px;display:flex;justify-content:space-between;align-items:center}
.iframe-box{position:relative;width:100%;padding-top:60%;overflow:hidden}
.iframe-box iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
transform:scale(.7);transform-origin:top left;border:0}
.footer{padding:10px 16px;background:rgba(255,255,255,.85);backdrop-filter:blur(6px);text-align:right}
.footer a{font-size:.85rem;font-weight:600;color:#2c5282;text-decoration:none}
.pg{display:flex;justify-content:center;gap:10px;margin-top:10px}
.pg button{border:1px solid #ddd;background:white;padding:6px 14px;border-radius:8px;cursor:pointer}
.pg button.active,.pg button:hover{background:#9c89b8;color:white;border-color:#9c89b8}
</style>
"""
html = css + "<div class='grid'>"
for g in games:
date = datetime.datetime.fromtimestamp(g["timestamp"]).strftime("%Y-%m-%d")
html += f"""
<div class='item'>
<div class='hdr'>
<p class='ttl'>{g['title']}</p>
<div class='meta'><span>{date}</span></div>
</div>
<div class='iframe-box'>
<iframe src="{g['url']}" loading="lazy"
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe>
</div>
<div class='footer'>
<a href="{g['url']}" target="_blank">์›๋ณธ ์‚ฌ์ดํŠธ ์—ด๊ธฐ โ†—</a>
</div>
</div>
"""
#โ€ŠํŽ˜์ด์ง€๋„ค์ด์…˜
html += "</div>"
if total_pages > 1:
html += "<div class='pg'>"
if page > 1:
html += (
f"<button onclick=\"refreshGallery('{tab_name}',{page-1})\">์ด์ „</button>"
)
for p in range(max(1, page - 2), min(total_pages, page + 2) + 1):
active = "active" if p == page else ""
html += (
f"<button class='{active}' onclick=\"refreshGallery('{tab_name}',{p})\">{p}</button>"
)
if page < total_pages:
html += (
f"<button onclick=\"refreshGallery('{tab_name}',{page+1})\">๋‹ค์Œ</button>"
)
html += "</div>"
#โ€ŠrefreshGallery JS (ํƒญ ๋ฒ„ํŠผ ์žฌ์‚ฌ์šฉ)
html += """
<script>
function refreshGallery(t,p){document.getElementById(t+'-btn').click();
window.setTimeout(()=>window.__setPage&&__setPage(t,p),50);}
</script>
"""
return html
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 7. Gradio UI
def create_gallery_interface():
initialize_best_games()
with gr.Blocks() as demo:
gr.HTML("<h1 style='text-align:center;'>๐ŸŽฎ Vibe Game Craft</h1>")
with gr.Row():
new_btn = gr.Button("NEW", elem_id="new-btn")
best_btn = gr.Button("BEST", elem_id="best-btn")
refresh = gr.Button("๐Ÿ”„ ์ƒˆ๋กœ๊ณ ์นจ")
tab_state = gr.State("new")
page_state = gr.State(1) # NEW ํƒญ ํ˜„์žฌ ํŽ˜์ด์ง€
page_state2 = gr.State(1) # BEST ํƒญ ํ˜„์žฌ ํŽ˜์ด์ง€
gallery_out = gr.HTML()
#โ€Š๊ฐ ํƒญ ๋ Œ๋” ํ•จ์ˆ˜
def render_new(page=1):
games, total = paginate(get_latest_deployments(), page)
return generate_gallery_html(games, page, total, "new"), "new", page
def render_best(page=1):
games, total = paginate(load_best_games(), page)
return generate_gallery_html(games, page, total, "best"), "best", page
new_btn.click(fn=render_new, outputs=[gallery_out, tab_state, page_state])
best_btn.click(fn=render_best, outputs=[gallery_out, tab_state, page_state2])
#โ€Š์ƒˆ๋กœ๊ณ ์นจ
def do_refresh(tab, p1, p2):
if tab == "new":
return render_new(p1)[0]
return render_best(p2)[0]
refresh.click(fn=do_refresh,
inputs=[tab_state, page_state, page_state2],
outputs=[gallery_out])
#โ€ŠํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์œ„ํ•œ JS ์ฝœ๋ฐฑ์šฉ ์ˆจ์€ ํ•จ์ˆ˜
def set_page(tab, p):
if tab == "new":
return render_new(p)
return render_best(p)
#โ€Š์ดˆ๊ธฐ NEW
demo.load(fn=render_new, outputs=[gallery_out, tab_state, page_state])
#โ€ŠJS ์—์„œ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•จ์ˆ˜ ๋…ธ์ถœ
demo.set_event_trigger("__setPage", set_page, [tab_state, page_state, page_state2], [gallery_out, tab_state, page_state, page_state2])
return demo
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 8. ์‹คํ–‰ (Gradio Spaces ์ž๋™ ๊ฐ์ง€๋ฅผ ์œ„ํ•ด ์ „์—ญ app ๋ณ€์ˆ˜ ๋…ธ์ถœ)
app = create_gallery_interface()
if __name__ == "__main__":
app.launch()