import os, re, time, json, datetime, requests, gradio as gr # ───────────────────── 1. 기본 설정 ───────────────────── BEST_FILE, PER_PAGE = "best_games.json", 9 # ───────────────────── 2. BEST 데이터 ──────────────────── def _init_best(): if not os.path.exists(BEST_FILE): json.dump([], open(BEST_FILE, "w"), ensure_ascii=False) def _load_best(): """best_games.json → ["https://foo.vercel.app", ...] 형태로 로드""" try: raw = json.load(open(BEST_FILE)) if isinstance(raw, list): urls = [] for it in raw: if isinstance(it, str): urls.append(it) elif isinstance(it, dict) and "url" in it: urls.append(it["url"]) return urls return [] except Exception as e: print(f"BEST 데이터 로드 오류: {e}") return [] def _save_best(url_list: list[str]) -> bool: try: json.dump(url_list, open(BEST_FILE, "w"), ensure_ascii=False, indent=2) return True except Exception as e: print(f"BEST 데이터 저장 오류: {e}") return False # ───────────────────── 3. URL 추가 기능 ───────────────────── def add_url_to_best(url: str) -> bool: try: data = _load_best() if url in data: print("이미 존재:", url) return False data.insert(0, url) return _save_best(data) except Exception as e: print("URL 추가 오류:", e) return False # ───────────────────── 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. URL → iframe 변환 ─────────────── def process_url_for_iframe(url): is_huggingface = False embed_urls = [] if "huggingface.co/spaces" in url: is_huggingface = True base_url = url.rstrip("/") try: path = base_url.split("/spaces/")[1] owner, *rest = path.split("/") if rest: name = rest[0] clean_owner = owner.lower() clean_name = name.replace('.', '-').replace('_', '-').lower() embed_urls.append(f"https://huggingface.co/spaces/{owner}/{name}/embed") embed_urls.append(f"https://{clean_owner}-{clean_name}.hf.space") else: embed_urls.append(f"https://huggingface.co/spaces/{owner}/embed") except Exception: if not base_url.endswith("/embed"): embed_urls.append(f"{base_url}/embed") else: embed_urls.append(base_url) elif ".hf.space" in url: is_huggingface = True embed_urls.append(url) else: return url, False, [] primary = embed_urls[0] if embed_urls else url return primary, is_huggingface, embed_urls[1:] # ───────────────────── 6. HTML 렌더 ─────────────────────── def html(urls, pg, total): if not urls: return "
표시할 배포가 없습니다.
" css = r""" """ js = """ """ body = '
' for i, url in enumerate(urls): iframe_url, is_hf, alt = process_url_for_iframe(url) frame_cls = "frame huggingface" if is_hf else "frame" iframe_id = f"f{i}" alt_attr = f'data-alt="{",".join(alt)}"' if alt else "" body += f"""
""" body += "
" page_info = f'
Page {pg} / {total}
' return css + js + body + page_info # ───────────────────── 7. UI ───────────────────────────── def build(): _init_best() header_html = """

🎮 Vibe Game Gallery

""" css_global = """ footer{display:none !important;} .button-row{position:fixed;bottom:0;left:0;right:0;height:60px;background:#f0f0f0;padding:10px;text-align:center;box-shadow:0 -2px 10px rgba(0,0,0,.05);z-index:1000;} .button-row button{margin:0 10px;padding:10px 20px;font-size:16px;font-weight:bold;border-radius:50px;} #content-area{overflow-y:auto;height:calc(100vh - 60px - 120px);} """ with gr.Blocks(title="Vibe Game Gallery", css=css_global) as demo: gr.HTML(header_html) out = gr.HTML(elem_id="content-area") with gr.Row(elem_classes="button-row"): b_prev = gr.Button("◀ 이전", size="lg") b_next = gr.Button("다음 ▶", size="lg") bp = gr.State(1) def show_best(p=1): d, t = page(_load_best(), p) return html(d, p, t), p def prev(b): b = max(1, b-1) h, _ = show_best(b) return h, b def nxt(b): maxp = (len(_load_best()) + PER_PAGE - 1) // PER_PAGE b = min(maxp, b+1) h, _ = show_best(b) return h, b b_prev.click(prev, inputs=[bp], outputs=[out, bp]) b_next.click(nxt, inputs=[bp], outputs=[out, bp]) demo.load(show_best, outputs=[out, bp]) return demo app = build() if __name__ == "__main__": app.launch()