Game-Gallery / app.py
ginipick's picture
Update app.py
d68f6ae verified
raw
history blame
13.1 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")
BYPASS_SECRET = os.getenv("VERCEL_AUTOMATION_BYPASS_SECRET") # ์ž๋™ํ™” ์šฐํšŒ ๋น„๋ฐ€ํ‚ค ์ถ”๊ฐ€
if not TOKEN:
raise EnvironmentError("SVR_TOKEN ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.")
API = "https://api.vercel.com"
HEAD = {"Authorization": f"Bearer {TOKEN}"}
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 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 []
params = {"limit": limit}
# ํŒ€ 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'])}๊ฐœ์˜ ๋ฐฐํฌ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค")
# ํŒจํ„ด ์™„ํ™”: vercel.app์œผ๋กœ ๋๋‚˜๋Š” ๋ชจ๋“  URL ํ—ˆ์šฉ
domain_pat = re.compile(r"\.vercel\.app$", re.I)
games = []
for d in data.get("deployments", []):
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:
games.append({
"title": d.get("name", "(์ œ๋ชฉ ์—†์Œ)"),
"url": f"https://{matched_url}",
"ts": int(d.get("created", time.time() * 1000) / 1000),
"deploymentId": d.get("uid", "") # ๋ฐฐํฌ ID ์ €์žฅ
})
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 originalUrl = iframe.src;
iframe.style.display = 'none';
const errorDiv = document.createElement('div');
errorDiv.className = 'frame-error';
// ์šฐํšŒ ๋น„๋ฐ€ํ‚ค๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ URL์— ์ถ”๊ฐ€ (Protection Bypass)
let bypassUrl = originalUrl;
if (typeof BYPASS_SECRET !== 'undefined' && BYPASS_SECRET) {
// URL์— ์ด๋ฏธ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
const hasQuery = bypassUrl.includes('?');
bypassUrl = bypassUrl + (hasQuery ? '&' : '?') + 'bypass=' + encodeURIComponent(BYPASS_SECRET);
}
errorDiv.innerHTML = '<div class="frame-error-icon">๐Ÿ”’</div>' +
'<div><strong>๋ณดํ˜ธ๋œ ๋ฐฐํฌ์ž…๋‹ˆ๋‹ค</strong></div>' +
'<div>๋ณด์•ˆ ์„ค์ •์œผ๋กœ ์ธํ•ด iframe์—์„œ ํ‘œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</div>' +
'<button class="frame-error-btn" onclick="window.open(\'' + bypassUrl + '\', \'_blank\')">์ƒˆ ํƒญ์—์„œ ์—ด๊ธฐ</button>';
container.appendChild(errorDiv);
}
// iframe ๋กœ๋“œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
function handleIframeLoad(iframe) {
try {
// CORS ์˜ค๋ฅ˜ ํƒ์ง€ ์‹œ๋„
if(iframe.contentWindow.location.href) {
iframe.style.opacity = 1;
}
} catch (e) {
// CORS ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด handleIframeError ํ˜ธ์ถœ
console.log("CORS ์˜ค๋ฅ˜ ๋˜๋Š” ๋ณดํ˜ธ๋œ ๋ฐฐํฌ:", e);
handleIframeError(iframe);
}
}
// iframe ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ
document.addEventListener('DOMContentLoaded', function() {
// ์ž๋™ํ™” ์šฐํšŒ ๋น„๋ฐ€ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด ์ „์—ญ ๋ณ€์ˆ˜๋กœ ์„ค์ •
const bypassSecret = "BYPASS_SECRET_PLACEHOLDER";
if (bypassSecret && bypassSecret !== "BYPASS_SECRET_PLACEHOLDER") {
window.BYPASS_SECRET = bypassSecret;
}
// ๋ชจ๋“  iframe์— ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€
const frames = document.querySelectorAll('iframe');
frames.forEach(frame => {
// URL์— ์šฐํšŒ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ (Protection Bypass)
if (window.BYPASS_SECRET) {
let url = frame.src;
const hasQuery = url.includes('?');
url = url + (hasQuery ? '&' : '?') + 'bypass=' + encodeURIComponent(window.BYPASS_SECRET);
frame.src = url;
}
// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
frame.addEventListener('error', function() {
handleIframeError(this);
});
frame.addEventListener('load', function() {
handleIframeLoad(this);
});
// 5์ดˆ ํ›„์—๋„ ๋กœ๋“œ๋˜์ง€ ์•Š์œผ๋ฉด ์˜ค๋ฅ˜๋กœ ๊ฐ„์ฃผ
setTimeout(function() {
if(frame.style.opacity !== '1') {
handleIframeError(frame);
}
}, 5000);
});
});
</script>"""
# ์šฐํšŒ ๋น„๋ฐ€ํ‚ค๊ฐ€ ์žˆ๋‹ค๋ฉด JavaScript์— ์ฃผ์ž…
bypass_script = ""
if BYPASS_SECRET:
bypass_script = f"<script>window.BYPASS_SECRET = '{BYPASS_SECRET}';</script>"
css = css.replace("BYPASS_SECRET_PLACEHOLDER", BYPASS_SECRET)
h = css + bypass_script + "<div class='grid'>"
for c in cards:
date=datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d")
# URL์— protection bypass ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
url = c['url']
if BYPASS_SECRET:
url_separator = '&' if '?' in url else '?'
url = f"{url}{url_separator}bypass={BYPASS_SECRET}"
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="{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="{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()