Spaces:
Running
Running
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() |