Game-Gallery / app.py
ginipick's picture
Update app.py
0dc8807 verified
raw
history blame
13.4 kB
import os, re, time, json, datetime, requests, gradio as gr
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. Vercel API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
TOKEN = os.getenv("SVR_TOKEN")
TEAM = os.getenv("VERCEL_TEAM_ID")
TEAM_SLUG = os.getenv("VERCEL_TEAM_SLUG") # ํŒ€ ์Šฌ๋Ÿฌ๊ทธ๋„ ์ถ”๊ฐ€
# ํŒ€ ID ๋˜๋Š” ์Šฌ๋Ÿฌ๊ทธ ํ™•์ธ
if not TEAM and TEAM_SLUG:
print(f"ํŒ€ ID ๋Œ€์‹  ํŒ€ ์Šฌ๋Ÿฌ๊ทธ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค: {TEAM_SLUG}")
elif not TEAM:
# ์‹ค์ œ ํŒ€ ID๋กœ ๊ต์ฒดํ•˜์„ธ์š”
TEAM = "team_YOUR_ACTUAL_TEAM_ID_HERE"
print(f"ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ํŒ€ ID๊ฐ€ ์—†์–ด์„œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค: {TEAM}")
if not TOKEN:
raise EnvironmentError("SVR_TOKEN ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.")
API = "https://api.vercel.com"
HEAD = {"Authorization": f"Bearer {TOKEN}"}
print(f"์‚ฌ์šฉ ์ค‘์ธ ํŒ€ ์ •๋ณด: ID={TEAM if TEAM else '์—†์Œ'}, Slug={TEAM_SLUG if TEAM_SLUG else '์—†์Œ'}")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 2. BEST ๋ฐ์ดํ„ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
BEST_FILE, PER_PAGE = "best_games.json", 48
def _init_best():
if not os.path.exists(BEST_FILE):
json.dump([], open(BEST_FILE, "w"))
def _load_best():
try:
data = json.load(open(BEST_FILE))
for it in data:
if "ts" not in it:
it["ts"] = int(it.get("timestamp", time.time()))
return data
except Exception:
return []
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. NEW: Vercel.app ๋ฐฐํฌ โ”€โ”€โ”€โ”€โ”€
# ํŒจํ„ด ์™„ํ™”: ๋ชจ๋“  vercel.app ๋„๋ฉ”์ธ ํ—ˆ์šฉ
PAT = re.compile(r"^[a-z0-9-]{1,}\.vercel\.app$", re.I)
def fetch_all(limit=200):
"""
Vercel ๋ฐฐํฌ ์ค‘ vercel.app ๋„๋ฉ”์ธ์„ ๊ฐ€์ง„ ๊ฒƒ์„ ์ˆ˜์ง‘
"""
try:
# ํ† ํฐ ๊ฒ€์ฆ
if not TOKEN:
print("์˜ค๋ฅ˜: API ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
return []
# API ๋ฌธ์„œ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
params = {
"limit": limit,
# ์ƒํƒœ๊ฐ€ "READY"์ธ ๋ฐฐํฌ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ ํ•„ํ„ฐ๋ง
"state": "READY"
}
# ํŒ€ ID ๋˜๋Š” ์Šฌ๋Ÿฌ๊ทธ ์ฒ˜๋ฆฌ
if TEAM:
params["teamId"] = TEAM
print(f"ํŒ€ ID๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค: {TEAM}")
elif TEAM_SLUG:
params["slug"] = TEAM_SLUG
print(f"ํŒ€ ์Šฌ๋Ÿฌ๊ทธ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค: {TEAM_SLUG}")
else:
print("๊ฒฝ๊ณ : ํŒ€ ์ •๋ณด๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ๋งŒ ์กฐํšŒ๋ฉ๋‹ˆ๋‹ค.")
print(f"Vercel API ํ˜ธ์ถœ: {API}/v6/deployments (params={params})")
resp = requests.get(f"{API}/v6/deployments",
headers=HEAD, params=params, timeout=30)
# ์ƒ์„ธํ•œ ์‘๋‹ต ์ •๋ณด ์ถœ๋ ฅ
print(f"API ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ: {resp.status_code}")
if resp.status_code != 200:
error_msg = f"API ์‘๋‹ต ์˜ค๋ฅ˜: {resp.status_code}"
if hasattr(resp, 'text'):
error_msg += f", {resp.text[:200]}"
print(error_msg)
return []
data = resp.json()
if "deployments" not in data:
print(f"API ์‘๋‹ต์— deployments ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: {str(data)[:200]}...")
return []
print(f"{len(data['deployments'])}๊ฐœ์˜ ๋ฐฐํฌ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค")
# ๋””๋ฒ„๊น…: ์ฒซ ๋ฒˆ์งธ ์‘๋‹ต ํ˜•์‹ ํ™•์ธ
if data["deployments"]:
first = data["deployments"][0]
print(f"\n์ฒซ ๋ฒˆ์งธ ๋ฐฐํฌ URL: {first.get('url')}")
print(f"์ฒซ ๋ฒˆ์งธ ๋ฐฐํฌ Alias: {first.get('alias', [])}\n")
print(f"์ฒซ ๋ฒˆ์งธ ๋ฐฐํฌ ์ƒํƒœ: {first.get('state')}\n")
# ํŒจํ„ด ์™„ํ™”: vercel.app์œผ๋กœ ๋๋‚˜๋Š” ๋ชจ๋“  URL ํ—ˆ์šฉ
domain_pat = re.compile(r"\.vercel\.app$", re.I)
games = []
for d in data.get("deployments", []):
# ๋ฌธ์„œ์—์„œ๋Š” state ํ•„ํ„ฐ๋ง์„ URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ–ˆ์ง€๋งŒ,
# ์ถ”๊ฐ€ ๊ฒ€์ฆ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค
if d.get("state") != "READY":
continue
# URL์ด๋‚˜ alias ์ค‘ ํ•˜๋‚˜๋ผ๋„ vercel.app์œผ๋กœ ๋๋‚˜๋ฉด ํฌํ•จ
url = d.get("url", "")
aliases = d.get("alias", [])
matched_url = None
if url and domain_pat.search(url):
matched_url = url
else:
for alias in aliases:
if domain_pat.search(alias):
matched_url = alias
break
if matched_url:
# ์‹œ๊ฐ„ ํ˜•์‹ ๋ณ€ํ™˜ ๊ฐœ์„ 
created_time = d.get("created")
if created_time:
# milliseconds to seconds ๋ณ€ํ™˜
try:
ts = int(created_time / 1000)
except:
ts = int(time.time())
else:
ts = int(time.time())
games.append({
"title": d.get("name", "(์ œ๋ชฉ ์—†์Œ)"),
"url": f"https://{matched_url}",
"ts": ts,
"meta": { # ์ถ”๊ฐ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
"projectId": d.get("projectId"),
"target": d.get("target"),
"deploymentId": d.get("uid")
}
})
print(f"vercel.app ๋„๋ฉ”์ธ ๋ฐฐํฌ {len(games)}๊ฐœ๋ฅผ ํ•„ํ„ฐ๋งํ–ˆ์Šต๋‹ˆ๋‹ค")
return sorted(games, key=lambda x: x["ts"], reverse=True)
except Exception as e:
print(f"Vercel API ์˜ค๋ฅ˜: {str(e)}")
import traceback
traceback.print_exc()
return []
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. ํŽ˜์ด์ง€๋„ค์ด์…˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def page(lst, pg):
s=(pg-1)*PER_PAGE; e=s+PER_PAGE
total=(len(lst)+PER_PAGE-1)//PER_PAGE
return lst[s:e], total
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 5. HTML 3-์—ด ๊ทธ๋ฆฌ๋“œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def html(cards, pg, total):
if not cards:
return "<div style='text-align:center;padding:70px;color:#555;'>ํ‘œ์‹œํ•  ๋ฐฐํฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</div>"
css=r"""
<style>
body{margin:0;font-family:Poppins,sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px 24px;margin:0 20px 60px;}
@media(max-width:1024px){.grid{grid-template-columns:repeat(2,1fr);} }
@media(max-width:640px){ .grid{grid-template-columns:1fr;} }
.card{background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 10px 25px rgba(0,0,0,.08);transition:.3s}
.card:hover{transform:translateY(-6px);box-shadow:0 16px 40px rgba(0,0,0,.12)}
.hdr{padding:20px 24px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee;}
.ttl{margin:0;font-size:1.15rem;font-weight:700;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.date{margin-top:4px;font-size:.85rem;color:#777;}
.frame{position:relative;width:100%;padding-top:60%;overflow:hidden;}
.frame iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
transform:scale(.7);transform-origin:top left;border:0;opacity:0;transition:opacity 0.3s;}
.foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
.link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
.cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;}
/* ์ถ”๊ฐ€ - iframe ์˜ค๋ฅ˜ ์ƒํƒœ ์ฒ˜๋ฆฌ */
.frame-error {position:absolute;top:0;left:0;width:100%;height:100%;
display:flex;align-items:center;justify-content:center;
background:#f8f8f8;color:#666;font-size:14px;text-align:center;
padding:20px;box-sizing:border-box;flex-direction:column;}
.frame-error-icon {font-size:36px;margin-bottom:12px;color:#999;}
.frame-error-btn {margin-top:12px;padding:6px 16px;background:#4a6dd8;color:white;
border-radius:20px;font-size:12px;cursor:pointer;border:none;
transition:all 0.2s ease;}
.frame-error-btn:hover {background:#3a5dc8;transform:scale(1.05);}
</style>
<script>
// iframe ๋กœ๋“œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
function handleIframeError(iframe) {
const container = iframe.parentNode;
const url = iframe.src;
iframe.style.display = 'none';
const errorDiv = document.createElement('div');
errorDiv.className = 'frame-error';
errorDiv.innerHTML = '<div class="frame-error-icon">๐Ÿ”’</div>' +
'<div><strong>์ ‘๊ทผ์ด ์ œํ•œ๋œ ๋ฐฐํฌ์ž…๋‹ˆ๋‹ค</strong></div>' +
'<div>๋ณด์•ˆ ์„ค์ •์œผ๋กœ ์ธํ•ด iframe์—์„œ ํ‘œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</div>' +
'<button class="frame-error-btn" onclick="window.open(\'' + url + '\', \'_blank\')">์ƒˆ ํƒญ์—์„œ ์—ด๊ธฐ</button>';
container.appendChild(errorDiv);
}
// iframe ๋กœ๋“œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
function handleIframeLoad(iframe) {
try {
// CORS ์˜ค๋ฅ˜ ํƒ์ง€ ์‹œ๋„ - ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ iframe ์ ‘๊ทผ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ
if(iframe.contentWindow.location.href) {
iframe.style.opacity = 1;
}
} catch (e) {
// CORS ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด handleIframeError ํ˜ธ์ถœ
console.log("CORS ์˜ค๋ฅ˜ ๋˜๋Š” ์ ‘๊ทผ ๊ฑฐ๋ถ€๋จ:", e);
handleIframeError(iframe);
}
}
// iframe ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ
document.addEventListener('DOMContentLoaded', function() {
// ๋ชจ๋“  iframe์— ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€
const frames = document.querySelectorAll('iframe');
frames.forEach(frame => {
// ์˜ค๋ฅ˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
frame.addEventListener('error', function() {
console.log("iframe ๋กœ๋“œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ");
handleIframeError(this);
});
// ๋กœ๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
frame.addEventListener('load', function() {
console.log("iframe ๋กœ๋“œ๋จ:", this.src);
handleIframeLoad(this);
});
// 5์ดˆ ํ›„์—๋„ ๋กœ๋“œ๋˜์ง€ ์•Š์œผ๋ฉด ์˜ค๋ฅ˜๋กœ ๊ฐ„์ฃผ
setTimeout(function() {
if(frame.style.opacity !== '1') {
console.log("iframe ๋กœ๋“œ ํƒ€์ž„์•„์›ƒ:", frame.src);
handleIframeError(frame);
}
}, 5000);
});
});
</script>"""
h=css+"<div class='grid'>"
for c in cards:
date=datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d")
h+=f"""
<div class='card'>
<div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
<div class='frame'>
<iframe src="{c['url']}" loading="lazy"
referrerpolicy="no-referrer"
sandbox="allow-scripts allow-same-origin allow-forms"
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe>
</div>
<div class='foot'><a class='link' href="{c['url']}" target="_blank">์›๋ณธโ†—</a></div>
</div>"""
h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
return h
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. Gradio Blocks UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def build():
_init_best()
with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
gr.Markdown("<h1 style='text-align:center;padding:32px 0 0;color:#333;'>๐ŸŽฎ Vibe Game Craft</h1>")
with gr.Row():
b_new = gr.Button("NEW", size="sm")
b_best = gr.Button("BEST", size="sm")
b_prev = gr.Button("โฌ…๏ธ Prev", size="sm")
b_next = gr.Button("Next โžก๏ธ", size="sm")
b_ref = gr.Button("๐Ÿ”„ Reload", size="sm")
tab = gr.State("new"); np = gr.State(1); bp = gr.State(1); out = gr.HTML()
def show_new(p=1): d,t=page(fetch_all(),p); return html(d,p,t),"new",p
def show_best(p=1): d,t=page(_load_best(),p); return html(d,p,t),"best",p
def prev(t,n,b):
if t=="new": n=max(1,n-1); h,_,_=show_new(n); return h,n,b
b=max(1,b-1); h,_,_=show_best(b); return h,n,b
def nxt(t,n,b):
if t=="new":
maxp=(len(fetch_all())+PER_PAGE-1)//PER_PAGE
n=min(maxp,n+1); h,_,_=show_new(n); return h,n,b
maxp=(len(_load_best())+PER_PAGE-1)//PER_PAGE
b=min(maxp,b+1); h,_,_=show_best(b); return h,n,b
b_new.click(show_new, outputs=[out,tab,np])
b_best.click(show_best,outputs=[out,tab,bp])
b_prev.click(prev, inputs=[tab,np,bp], outputs=[out,np,bp])
b_next.click(nxt, inputs=[tab,np,bp], outputs=[out,np,bp])
b_ref.click(lambda t,n,b: show_new(n)[0] if t=="new" else show_best(b)[0],
inputs=[tab,np,bp], outputs=[out])
demo.load(show_new, outputs=[out,tab,np])
return demo
app = build()
if __name__ == "__main__":
app.launch()