import os import json import re import gradio as gr # ───────────────────── 1. 기본 설정 ───────────────────── BEST_FILE, PER_PAGE = "best_games.json", 9 # ❶ 페이지당 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(): try: raw = json.load(open(BEST_FILE)) # URL 리스트만 반환 if isinstance(raw, list): return [u if isinstance(u, str) else u.get("url") for u in raw] return [] except Exception as e: print("BEST 로드 오류:", e) return [] def _save_best(lst): # URL 리스트 저장 try: json.dump(lst, open(BEST_FILE, "w"), ensure_ascii=False, indent=2) return True except Exception as e: print("BEST 저장 오류:", e) return False def to_hub_space_url(url: str) -> str: """ *.hf.space URL을 https://huggingface.co/spaces// 형식으로 변환 다른 URL은 그대로 반환 """ m = re.match(r"https?://([^-]+)-([^.]+)\.hf\.space(/.*)?", url) if m: owner, space, _ = m.groups() return f"https://huggingface.co/spaces/{owner}/{space}" return url def add_url_to_best(url: str): # 저장할 때는 사용자가 준 원본을 그대로 둔다 data = _load_best() if url in data: return False data.insert(0, url) return _save_best(data) # ───────────────────── 3. 유틸 ────────────────────────── def page(lst, pg): s, e = (pg - 1) * PER_PAGE, (pg - 1) * PER_PAGE + PER_PAGE total = (len(lst) + PER_PAGE - 1) // PER_PAGE return lst[s:e], total def process_url_for_iframe(url): """ 반환: (iframe_url, extra_class, alternate_urls) extra_class : '' | 'huggingface' | 'hfspace' """ # Hugging Face Spaces embed (Gradio/Streamlit) if "huggingface.co/spaces" in url: owner, name = url.rstrip("/").split("/spaces/")[1].split("/")[:2] return f"https://huggingface.co/spaces/{owner}/{name}/embed", "huggingface", [] # *.hf.space (정적/static Space 포함) m = re.match(r"https?://([^/]+)\.hf\.space(/.*)?", url) if m: sub, rest = m.groups() static_url = f"https://{sub}.static.hf.space{rest or ''}" # alt_urls 에 원본 저장(실패 시 재시도 가능) return static_url, "hfspace", [url] return url, "", [] # ───────────────────── 6. HTML 그리드 ─────────────────── def html(cards, pg, total): if not cards: return ( "
" "표시할 배포가 없습니다.
" ) css = r""" """ js = """ """ h = css + js + '
' for idx, url in enumerate(cards): iframe_url, extra_cls, alt_urls = process_url_for_iframe(url) frame_class = f"frame {extra_cls}".strip() iframe_id = f"iframe-{idx}-{hash(url) % 10000}" alt_attr = f'data-alternate-urls="{",".join(alt_urls)}"' if alt_urls else "" safe_url = to_hub_space_url(url) # 새 탭 링크용 변환 URL h += f""" """ h += "
" h += f'
Page {pg} / {total}
' return h # ───────────────────── 5. Gradio UI ───────────────────── def build(): _init_best() header = """

🎮 Vibe Game Gallery

""" global_css = """ 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=global_css) as demo: gr.HTML(header) 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 render(p=1): data, tot = page(_load_best(), p) return html(data, p, tot), p b_prev.click(lambda p: render(max(1, p - 1)), inputs=bp, outputs=[out, bp]) b_next.click(lambda p: render(p + 1), inputs=bp, outputs=[out, bp]) demo.load(render, outputs=[out, bp]) return demo app = build() if __name__ == "__main__": app.launch()