ginipick commited on
Commit
15d13e8
ยท
verified ยท
1 Parent(s): 5a08d41

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -125
app.py CHANGED
@@ -3,14 +3,14 @@ import os, re, time, json, datetime, requests, gradio as gr
3
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. Vercel API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
4
  TOKEN = os.getenv("SVR_TOKEN")
5
  TEAM = os.getenv("VERCEL_TEAM_ID")
6
- TEAM_SLUG = os.getenv("VERCEL_TEAM_SLUG")
7
- BYPASS_SECRET = os.getenv("VERCEL_AUTOMATION_BYPASS_SECRET") # ์ž๋™ํ™” ์šฐํšŒ ๋น„๋ฐ€ํ‚ค ์ถ”๊ฐ€
8
-
9
  if not TOKEN:
10
  raise EnvironmentError("SVR_TOKEN ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.")
11
  API = "https://api.vercel.com"
12
  HEAD = {"Authorization": f"Bearer {TOKEN}"}
13
 
 
 
 
14
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 2. BEST ๋ฐ์ดํ„ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
  BEST_FILE, PER_PAGE = "best_games.json", 48
16
  def _init_best():
@@ -23,87 +23,78 @@ def _load_best():
23
  if "ts" not in it:
24
  it["ts"] = int(it.get("timestamp", time.time()))
25
  return data
26
- except Exception:
 
27
  return []
28
 
29
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. NEW: Vercel.app ๋ฐฐํฌ โ”€โ”€โ”€โ”€โ”€
30
- # ํŒจํ„ด ์™„ํ™”: ๋ชจ๋“  vercel.app ๋„๋ฉ”์ธ ํ—ˆ์šฉ
31
- PAT = re.compile(r"^[a-z0-9-]{1,}\.vercel\.app$", re.I)
32
-
33
- def fetch_all(limit=200):
34
  """
35
- Vercel ๋ฐฐํฌ ์ค‘ vercel.app ๋„๋ฉ”์ธ์„ ๊ฐ€์ง„ ๊ฒƒ์„ ์ˆ˜์ง‘
36
  """
37
  try:
38
- # ํ† ํฐ ๊ฒ€์ฆ
39
- if not TOKEN:
40
- print("์˜ค๋ฅ˜: API ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
41
- return []
42
-
43
  params = {"limit": limit}
44
-
45
- # ํŒ€ ID ๋˜๋Š” ์Šฌ๋Ÿฌ๊ทธ ์ฒ˜๋ฆฌ
46
  if TEAM:
47
  params["teamId"] = TEAM
48
- print(f"ํŒ€ ID๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค: {TEAM}")
49
- elif TEAM_SLUG:
50
- params["slug"] = TEAM_SLUG
51
- print(f"ํŒ€ ์Šฌ๋Ÿฌ๊ทธ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค: {TEAM_SLUG}")
52
- else:
53
- print("๊ฒฝ๊ณ : ํŒ€ ์ •๋ณด๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ๋งŒ ์กฐํšŒ๋ฉ๋‹ˆ๋‹ค.")
54
 
55
- print(f"Vercel API ํ˜ธ์ถœ: {API}/v6/deployments (params={params})")
56
 
57
- resp = requests.get(f"{API}/v6/deployments",
58
  headers=HEAD, params=params, timeout=30)
59
 
60
- # ์ƒ์„ธํ•œ ์‘๋‹ต ์ •๋ณด ์ถœ๋ ฅ
61
  print(f"API ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ: {resp.status_code}")
62
  if resp.status_code != 200:
63
- error_msg = f"API ์‘๋‹ต ์˜ค๋ฅ˜: {resp.status_code}"
64
- if hasattr(resp, 'text'):
65
- error_msg += f", {resp.text[:200]}"
66
- print(error_msg)
67
  return []
68
 
69
  data = resp.json()
70
 
71
- if "deployments" not in data:
72
- print(f"API ์‘๋‹ต์— deployments ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: {str(data)[:200]}...")
73
  return []
74
 
75
- print(f"{len(data['deployments'])}๊ฐœ์˜ ๋ฐฐํฌ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค")
76
-
77
- # ํŒจํ„ด ์™„ํ™”: vercel.app์œผ๋กœ ๋๋‚˜๋Š” ๋ชจ๋“  URL ํ—ˆ์šฉ
78
- domain_pat = re.compile(r"\.vercel\.app$", re.I)
79
 
 
80
  games = []
81
- for d in data.get("deployments", []):
82
- if d.get("state") != "READY":
83
- continue
84
-
85
- # URL์ด๋‚˜ alias ์ค‘ ํ•˜๋‚˜๋ผ๋„ vercel.app์œผ๋กœ ๋๋‚˜๋ฉด ํฌํ•จ
86
- url = d.get("url", "")
87
- aliases = d.get("alias", [])
88
-
89
- matched_url = None
90
- if url and domain_pat.search(url):
91
- matched_url = url
92
- else:
93
- for alias in aliases:
94
- if domain_pat.search(alias):
95
- matched_url = alias
96
- break
97
 
98
- if matched_url:
99
- games.append({
100
- "title": d.get("name", "(์ œ๋ชฉ ์—†์Œ)"),
101
- "url": f"https://{matched_url}",
102
- "ts": int(d.get("created", time.time() * 1000) / 1000),
103
- "deploymentId": d.get("uid", "") # ๋ฐฐ๏ฟฝ๏ฟฝ๏ฟฝ ID ์ €์žฅ
104
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- print(f"vercel.app ๋„๋ฉ”์ธ ๋ฐฐํฌ {len(games)}๊ฐœ๋ฅผ ํ•„ํ„ฐ๋งํ–ˆ์Šต๋‹ˆ๋‹ค")
107
  return sorted(games, key=lambda x: x["ts"], reverse=True)
108
 
109
  except Exception as e:
@@ -112,7 +103,6 @@ def fetch_all(limit=200):
112
  traceback.print_exc()
113
  return []
114
 
115
-
116
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. ํŽ˜์ด์ง€๋„ค์ด์…˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
117
  def page(lst, pg):
118
  s=(pg-1)*PER_PAGE; e=s+PER_PAGE
@@ -134,70 +124,13 @@ def html(cards, pg, total):
134
  .hdr{padding:20px 24px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee;}
135
  .ttl{margin:0;font-size:1.15rem;font-weight:700;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
136
  .date{margin-top:4px;font-size:.85rem;color:#777;}
137
- .frame{position:relative;width:100%;padding-top:60%;overflow:hidden;background:#f7f7f7;}
138
  .frame iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
139
- transform:scale(.7);transform-origin:top left;border:0;opacity:0;transition:opacity 0.3s;}
140
- .frame-error {position:absolute;top:0;left:0;width:100%;height:100%;
141
- display:flex;align-items:center;justify-content:center;
142
- background:#f8f8f8;color:#666;font-size:14px;text-align:center;
143
- padding:20px;box-sizing:border-box;flex-direction:column;}
144
- .frame-error-icon {font-size:36px;margin-bottom:12px;color:#999;}
145
- .frame-error-btn {margin-top:12px;padding:6px 16px;background:#4a6dd8;color:white;
146
- border-radius:20px;font-size:12px;cursor:pointer;border:none;
147
- transition:all 0.2s ease;}
148
- .frame-error-btn:hover {background:#3a5dc8;transform:scale(1.05);}
149
  .foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
150
  .link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
151
  .cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;}
152
- </style>
153
-
154
- <script>
155
- document.addEventListener('DOMContentLoaded', function() {
156
- // iframe ๋กœ๋“œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
157
- function handleIframeError(iframe) {
158
- const container = iframe.parentNode;
159
- const url = iframe.src;
160
- iframe.style.display = 'none';
161
- const errorDiv = document.createElement('div');
162
- errorDiv.className = 'frame-error';
163
- errorDiv.innerHTML = '<div class="frame-error-icon">๐ŸŽฎ</div>' +
164
- '<div><strong>๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</strong></div>' +
165
- '<div>์ƒˆ ํƒญ์—์„œ ์ง์ ‘ ํ™•์ธํ•ด๋ณด์„ธ์š”</div>' +
166
- '<button class="frame-error-btn" onclick="window.open(\'' + url + '\', \'_blank\')">๊ฒŒ์ž„ ์—ด๊ธฐ</button>';
167
- container.appendChild(errorDiv);
168
- }
169
-
170
- // iframe ๋กœ๋“œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
171
- function handleIframeLoad(iframe) {
172
- try {
173
- // CORS ์˜ค๋ฅ˜ ํƒ์ง€ ์‹œ๋„
174
- if (iframe.contentWindow && iframe.contentWindow.location && iframe.contentWindow.location.href) {
175
- iframe.style.opacity = 1;
176
- } else {
177
- handleIframeError(iframe);
178
- }
179
- } catch (e) {
180
- handleIframeError(iframe);
181
- }
182
- }
183
-
184
- // ๋ชจ๋“  iframe ์ฒ˜๋ฆฌ
185
- const frames = document.querySelectorAll('iframe');
186
- frames.forEach(frame => {
187
- // iframe์ด ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ณด์ด๋„๋ก ๊ธฐ๋ณธ ์˜ค๋ฅ˜ UI ํ‘œ์‹œ
188
- handleIframeError(frame);
189
-
190
- // ์‹ค์ œ iframe ๋กœ๋“œ ์‹œ๋„
191
- frame.addEventListener('load', function() {
192
- handleIframeLoad(this);
193
- });
194
-
195
- frame.addEventListener('error', function() {
196
- handleIframeError(this);
197
- });
198
- });
199
- });
200
- </script>"""
201
 
202
  h=css+"<div class='grid'>"
203
  for c in cards:
@@ -205,14 +138,13 @@ def html(cards, pg, total):
205
  h+=f"""
206
  <div class='card'>
207
  <div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
208
- <div class='frame'>
209
- <iframe src="{c['url']}" loading="lazy"
210
- allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe>
211
- </div>
212
  <div class='foot'><a class='link' href="{c['url']}" target="_blank">์›๋ณธโ†—</a></div>
213
  </div>"""
214
  h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
215
  return h
 
216
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. Gradio Blocks UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
217
  def build():
218
  _init_best()
 
3
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. Vercel API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
4
  TOKEN = os.getenv("SVR_TOKEN")
5
  TEAM = os.getenv("VERCEL_TEAM_ID")
 
 
 
6
  if not TOKEN:
7
  raise EnvironmentError("SVR_TOKEN ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.")
8
  API = "https://api.vercel.com"
9
  HEAD = {"Authorization": f"Bearer {TOKEN}"}
10
 
11
+ print(f"API ํ† ํฐ: {TOKEN[:4]}... (๋งˆ์Šคํ‚น๋จ)")
12
+ print(f"ํŒ€ ID: {TEAM if TEAM else '์—†์Œ'}")
13
+
14
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 2. BEST ๋ฐ์ดํ„ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
  BEST_FILE, PER_PAGE = "best_games.json", 48
16
  def _init_best():
 
23
  if "ts" not in it:
24
  it["ts"] = int(it.get("timestamp", time.time()))
25
  return data
26
+ except Exception as e:
27
+ print(f"BEST ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜: {e}")
28
  return []
29
 
30
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. NEW: ํ”„๋กœ์ ํŠธ API ํ˜ธ์ถœ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
+ def fetch_all(limit=50):
 
 
 
32
  """
33
+ Vercel API v10/projects๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
34
  """
35
  try:
36
+ # API ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
 
 
 
 
37
  params = {"limit": limit}
 
 
38
  if TEAM:
39
  params["teamId"] = TEAM
 
 
 
 
 
 
40
 
41
+ print(f"Vercel API ํ˜ธ์ถœ (ํ”„๋กœ์ ํŠธ): {API}/v10/projects (params={params})")
42
 
43
+ resp = requests.get(f"{API}/v10/projects",
44
  headers=HEAD, params=params, timeout=30)
45
 
 
46
  print(f"API ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ: {resp.status_code}")
47
  if resp.status_code != 200:
48
+ print(f"API ์‘๋‹ต ์˜ค๋ฅ˜: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
 
 
 
49
  return []
50
 
51
  data = resp.json()
52
 
53
+ if "projects" not in data:
54
+ print(f"API ์‘๋‹ต์— projects ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: {str(data)[:200]}...")
55
  return []
56
 
57
+ print(f"{len(data['projects'])}๊ฐœ์˜ ํ”„๋กœ์ ํŠธ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค")
 
 
 
58
 
59
+ # ๊ฐ ํ”„๋กœ์ ํŠธ์˜ ์ตœ์‹  ๋ฐฐํฌ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
60
  games = []
61
+ for project in data.get("projects", []):
62
+ project_id = project.get("id")
63
+ project_name = project.get("name", "(์ œ๋ชฉ ์—†์Œ)")
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ # ๊ฐ ํ”„๋กœ์ ํŠธ์˜ ์ตœ์‹  ๋ฐฐํฌ ๊ฐ€์ ธ์˜ค๊ธฐ
66
+ try:
67
+ deploy_params = {"limit": 1, "target": "production"}
68
+ if TEAM:
69
+ deploy_params["teamId"] = TEAM
70
+
71
+ deploy_resp = requests.get(
72
+ f"{API}/v6/deployments",
73
+ headers=HEAD,
74
+ params=deploy_params,
75
+ timeout=30
76
+ )
77
+
78
+ if deploy_resp.status_code == 200:
79
+ deploy_data = deploy_resp.json()
80
+ deployments = deploy_data.get("deployments", [])
81
+
82
+ if deployments:
83
+ deployment = deployments[0]
84
+ if deployment.get("state") == "READY":
85
+ url = deployment.get("url", "")
86
+ if url:
87
+ games.append({
88
+ "title": project_name,
89
+ "url": f"https://{url}",
90
+ "ts": int(deployment.get("created", time.time() * 1000) / 1000),
91
+ "projectId": project_id
92
+ })
93
+ except Exception as e:
94
+ print(f"ํ”„๋กœ์ ํŠธ {project_id}์˜ ๋ฐฐํฌ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: {e}")
95
+ continue
96
 
97
+ print(f"์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฐํฌ {len(games)}๊ฐœ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค")
98
  return sorted(games, key=lambda x: x["ts"], reverse=True)
99
 
100
  except Exception as e:
 
103
  traceback.print_exc()
104
  return []
105
 
 
106
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. ํŽ˜์ด์ง€๋„ค์ด์…˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
  def page(lst, pg):
108
  s=(pg-1)*PER_PAGE; e=s+PER_PAGE
 
124
  .hdr{padding:20px 24px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee;}
125
  .ttl{margin:0;font-size:1.15rem;font-weight:700;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
126
  .date{margin-top:4px;font-size:.85rem;color:#777;}
127
+ .frame{position:relative;width:100%;padding-top:60%;overflow:hidden;}
128
  .frame iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
129
+ transform:scale(.7);transform-origin:top left;border:0;}
 
 
 
 
 
 
 
 
 
130
  .foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
131
  .link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
132
  .cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;}
133
+ </style>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  h=css+"<div class='grid'>"
136
  for c in cards:
 
138
  h+=f"""
139
  <div class='card'>
140
  <div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
141
+ <div class='frame'><iframe src="{c['url']}" loading="lazy"
142
+ allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
 
 
143
  <div class='foot'><a class='link' href="{c['url']}" target="_blank">์›๋ณธโ†—</a></div>
144
  </div>"""
145
  h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
146
  return h
147
+
148
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. Gradio Blocks UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
149
  def build():
150
  _init_best()