openfree commited on
Commit
9da8287
·
verified ·
1 Parent(s): 0f67f91

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +368 -0
app.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, re, json, sqlite3
2
+ from flask import Flask, render_template, request, jsonify
3
+
4
+ app = Flask(__name__)
5
+
6
+ # ────────────────────────── 경로를 절대 경로로 지정 ──────────────────────────
7
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
8
+ DB_FILE = os.path.join(BASE_DIR, "favorite_sites.json")
9
+ SQLITE_DB = os.path.join(BASE_DIR, "favorite_sites.db")
10
+
11
+ # 이하 기존 코드 동일
12
+ BLOCKED_DOMAINS = [
13
+ "naver.com", "daum.net", "google.com",
14
+ "facebook.com", "instagram.com", "kakao.com",
15
+ "ycombinator.com"
16
+ ]
17
+
18
+ CATEGORIES = {
19
+ "Productivity": [
20
+ "https://huggingface.co/spaces/ginigen/perflexity-clone",
21
+ "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
22
+ "https://huggingface.co/spaces/VIDraft/mouse-webgen",
23
+ "https://huggingface.co/spaces/openfree/Vibe-Game",
24
+ "https://huggingface.co/spaces/openfree/Game-Gallery",
25
+ "https://huggingface.co/spaces/aiqtech/Contributors-Leaderboard",
26
+ "https://huggingface.co/spaces/fantaxy/Model-Leaderboard",
27
+ "https://huggingface.co/spaces/fantaxy/Space-Leaderboard",
28
+ "https://huggingface.co/spaces/openfree/Korean-Leaderboard",
29
+ ],
30
+ "Multimodal": [
31
+ "https://huggingface.co/spaces/openfree/DreamO-video",
32
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
33
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
34
+ "https://huggingface.co/spaces/fantaxy/Sound-AI-SFX",
35
+ "https://huggingface.co/spaces/ginigen/SFX-Sound-magic",
36
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
37
+ "https://huggingface.co/spaces/aiqcamp/MCP-kokoro",
38
+ "https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
39
+ ],
40
+ "Professional": [
41
+ "https://huggingface.co/spaces/ginigen/blogger",
42
+ "https://huggingface.co/spaces/VIDraft/money-radar",
43
+ "https://huggingface.co/spaces/immunobiotech/drug-discovery",
44
+ "https://huggingface.co/spaces/immunobiotech/Gemini-MICHELIN",
45
+ "https://huggingface.co/spaces/Heartsync/Papers-Leaderboard",
46
+ "https://huggingface.co/spaces/VIDraft/PapersImpact",
47
+ "https://huggingface.co/spaces/ginipick/AgentX-Papers",
48
+ "https://huggingface.co/spaces/openfree/Cycle-Navigator",
49
+ ],
50
+ "Image": [
51
+ "https://huggingface.co/spaces/ginigen/interior-design",
52
+ "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
53
+ "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
54
+ "https://huggingface.co/spaces/ginigen/Every-Text",
55
+ "https://huggingface.co/spaces/ginigen/text3d-r1",
56
+ "https://huggingface.co/spaces/ginipick/FLUXllama",
57
+ "https://huggingface.co/spaces/Heartsync/FLUX-Vision",
58
+ "https://huggingface.co/spaces/ginigen/VisualCloze",
59
+ "https://huggingface.co/spaces/seawolf2357/Ghibli-Multilingual-Text-rendering",
60
+ "https://huggingface.co/spaces/ginigen/Ghibli-Meme-Studio",
61
+ "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
62
+ "https://huggingface.co/spaces/ginigen/3D-LLAMA",
63
+ ],
64
+ "LLM / VLM": [
65
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
66
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
67
+ "https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
68
+ "https://huggingface.co/spaces/aiqcamp/gemini-2.5-flash-preview",
69
+ "https://huggingface.co/spaces/openfree/qwen3-30b-a3b-research",
70
+ "https://huggingface.co/spaces/openfree/qwen3-235b-a22b-research",
71
+ "https://huggingface.co/spaces/openfree/Llama-4-Maverick-17B-Research",
72
+ ],
73
+ }
74
+
75
+ def init_db():
76
+ if not os.path.exists(DB_FILE):
77
+ with open(DB_FILE, "w", encoding="utf-8") as f:
78
+ json.dump([], f, ensure_ascii=False)
79
+
80
+ conn = sqlite3.connect(SQLITE_DB)
81
+ cursor = conn.cursor()
82
+ cursor.execute('''
83
+ CREATE TABLE IF NOT EXISTS urls (
84
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
85
+ url TEXT UNIQUE NOT NULL,
86
+ date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP
87
+ )
88
+ ''')
89
+ conn.commit()
90
+
91
+ # JSON에 있는 URL을 (초기화 시) SQLite로도 복사
92
+ json_urls = load_json()
93
+ if json_urls:
94
+ db_urls = load_db_sqlite()
95
+ for url in json_urls:
96
+ if url not in db_urls:
97
+ add_url_to_sqlite(url)
98
+
99
+ conn.close()
100
+
101
+ def load_json():
102
+ """Load URLs from JSON file"""
103
+ try:
104
+ with open(DB_FILE, "r", encoding="utf-8") as f:
105
+ raw = json.load(f)
106
+ return raw if isinstance(raw, list) else []
107
+ except Exception:
108
+ return []
109
+
110
+ def save_json(lst):
111
+ """Save URLs to JSON file"""
112
+ try:
113
+ with open(DB_FILE, "w", encoding="utf-8") as f:
114
+ json.dump(lst, f, ensure_ascii=False, indent=2)
115
+ return True
116
+ except Exception:
117
+ return False
118
+
119
+ def load_db_sqlite():
120
+ """Load URLs from SQLite"""
121
+ conn = sqlite3.connect(SQLITE_DB)
122
+ cursor = conn.cursor()
123
+ cursor.execute("SELECT url FROM urls ORDER BY date_added DESC")
124
+ urls = [row[0] for row in cursor.fetchall()]
125
+ conn.close()
126
+ return urls
127
+
128
+ def add_url_to_sqlite(url):
129
+ """Add a URL to SQLite database"""
130
+ conn = sqlite3.connect(SQLITE_DB)
131
+ cursor = conn.cursor()
132
+ try:
133
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
134
+ conn.commit()
135
+ success = True
136
+ except sqlite3.IntegrityError:
137
+ success = False
138
+ conn.close()
139
+ return success
140
+
141
+ def update_url_in_sqlite(old_url, new_url):
142
+ """Update a URL in SQLite database"""
143
+ conn = sqlite3.connect(SQLITE_DB)
144
+ cursor = conn.cursor()
145
+ try:
146
+ cursor.execute("UPDATE urls SET url = ? WHERE url = ?", (new_url, old_url))
147
+ if cursor.rowcount > 0:
148
+ conn.commit()
149
+ success = True
150
+ else:
151
+ success = False
152
+ except sqlite3.IntegrityError:
153
+ success = False
154
+ conn.close()
155
+ return success
156
+
157
+ def delete_url_from_sqlite(url):
158
+ """Delete a URL from SQLite database"""
159
+ conn = sqlite3.connect(SQLITE_DB)
160
+ cursor = conn.cursor()
161
+ cursor.execute("DELETE FROM urls WHERE url = ?", (url,))
162
+ if cursor.rowcount > 0:
163
+ conn.commit()
164
+ success = True
165
+ else:
166
+ success = False
167
+ conn.close()
168
+ return success
169
+
170
+ def load_db():
171
+ """Load URLs: 우선 SQLite에서 불러온 뒤, 없으면 JSON 불러오기"""
172
+ urls = load_db_sqlite()
173
+ if not urls:
174
+ urls = load_json()
175
+ for url in urls:
176
+ add_url_to_sqlite(url)
177
+ return urls
178
+
179
+ def save_db(lst):
180
+ """Save URLs to both SQLite and JSON"""
181
+ conn = sqlite3.connect(SQLITE_DB)
182
+ cursor = conn.cursor()
183
+ cursor.execute("DELETE FROM urls")
184
+ for url in lst:
185
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
186
+ conn.commit()
187
+ conn.close()
188
+ return save_json(lst)
189
+
190
+ def direct_url(hf_url):
191
+ m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
192
+ if not m:
193
+ return hf_url
194
+ owner, name = m.groups()
195
+ owner = owner.lower()
196
+ name = name.replace('.', '-').replace('_', '-').lower()
197
+ return f"https://{owner}-{name}.hf.space"
198
+
199
+ def screenshot_url(url):
200
+ return f"https://image.thum.io/get/fullpage/{url}"
201
+
202
+ def process_url_for_preview(url):
203
+ if any(d for d in BLOCKED_DOMAINS if d in url):
204
+ return screenshot_url(url), "snapshot"
205
+ if "vibe-coding-tetris" in url or "World-of-Tank-GAME" in url or "Minesweeper-Game" in url:
206
+ return screenshot_url(url), "snapshot"
207
+ try:
208
+ if "huggingface.co/spaces" in url:
209
+ parts = url.rstrip("/").split("/")
210
+ if len(parts) >= 5:
211
+ owner = parts[-2]
212
+ name = parts[-1]
213
+ embed_url = f"https://huggingface.co/spaces/{owner}/{name}/embed"
214
+ return embed_url, "iframe"
215
+ except Exception:
216
+ return screenshot_url(url), "snapshot"
217
+ return url, "iframe"
218
+
219
+ @app.route('/api/category')
220
+ def api_category():
221
+ cat = request.args.get('name', '')
222
+ urls = CATEGORIES.get(cat, [])
223
+ return jsonify([
224
+ {
225
+ "title": url.split('/')[-1],
226
+ "owner": url.split('/')[-2] if '/spaces/' in url else '',
227
+ "iframe": direct_url(url),
228
+ "shot": screenshot_url(url),
229
+ "hf": url
230
+ } for url in urls
231
+ ])
232
+
233
+ @app.route('/api/favorites')
234
+ def api_favorites():
235
+ urls = load_db()
236
+ page = int(request.args.get('page', 1))
237
+ per_page = int(request.args.get('per_page', 9))
238
+
239
+ total_pages = max(1, (len(urls) + per_page - 1) // per_page)
240
+ start = (page - 1) * per_page
241
+ end = min(start + per_page, len(urls))
242
+
243
+ urls_page = urls[start:end]
244
+ result = []
245
+ for url in urls_page:
246
+ try:
247
+ preview_url, mode = process_url_for_preview(url)
248
+ result.append({
249
+ "title": url.split('/')[-1],
250
+ "url": url,
251
+ "preview_url": preview_url,
252
+ "mode": mode
253
+ })
254
+ except Exception:
255
+ result.append({
256
+ "title": url.split('/')[-1],
257
+ "url": url,
258
+ "preview_url": screenshot_url(url),
259
+ "mode": "snapshot"
260
+ })
261
+ return jsonify({
262
+ "items": result,
263
+ "page": page,
264
+ "total_pages": total_pages
265
+ })
266
+
267
+ @app.route('/api/url/add', methods=['POST'])
268
+ def add_url():
269
+ url = request.form.get('url', '').strip()
270
+ if not url:
271
+ return jsonify({"success": False, "message": "URL is required"})
272
+
273
+ conn = sqlite3.connect(SQLITE_DB)
274
+ cursor = conn.cursor()
275
+ try:
276
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
277
+ conn.commit()
278
+ success = True
279
+ except sqlite3.IntegrityError:
280
+ success = False
281
+ except Exception as e:
282
+ print(f"SQLite error: {str(e)}")
283
+ success = False
284
+ finally:
285
+ conn.close()
286
+
287
+ if not success:
288
+ return jsonify({"success": False, "message": "URL already exists or could not be added"})
289
+
290
+ # JSON 파일에도 추가 (백업용)
291
+ data = load_json()
292
+ if url not in data:
293
+ data.insert(0, url)
294
+ save_json(data)
295
+
296
+ return jsonify({"success": True, "message": "URL added successfully"})
297
+
298
+ @app.route('/api/url/update', methods=['POST'])
299
+ def update_url():
300
+ old = request.form.get('old', '')
301
+ new = request.form.get('new', '').strip()
302
+
303
+ if not new:
304
+ return jsonify({"success": False, "message": "New URL is required"})
305
+
306
+ if not update_url_in_sqlite(old, new):
307
+ return jsonify({"success": False, "message": "URL not found or new URL already exists"})
308
+
309
+ data = load_json()
310
+ try:
311
+ idx = data.index(old)
312
+ data[idx] = new
313
+ save_json(data)
314
+ except ValueError:
315
+ data.append(new)
316
+ save_json(data)
317
+
318
+ return jsonify({"success": True, "message": "URL updated successfully"})
319
+
320
+ @app.route('/api/url/delete', methods=['POST'])
321
+ def delete_url():
322
+ url = request.form.get('url', '')
323
+
324
+ if not delete_url_from_sqlite(url):
325
+ return jsonify({"success": False, "message": "URL not found"})
326
+
327
+ data = load_json()
328
+ try:
329
+ data.remove(url)
330
+ save_json(data)
331
+ except ValueError:
332
+ pass
333
+
334
+ return jsonify({"success": True, "message": "URL deleted successfully"})
335
+
336
+ @app.route('/')
337
+ def home():
338
+ os.makedirs('templates', exist_ok=True)
339
+ with open('templates/index.html', 'w', encoding='utf-8') as fp:
340
+ fp.write(r'''<!DOCTYPE html>
341
+ <!-- 이하 HTML 템플릿은 기존 코드 그대로 유지 -->
342
+ <!-- ... -->
343
+ ''')
344
+ return render_template('index.html', cats=list(CATEGORIES.keys()))
345
+
346
+ init_db()
347
+
348
+ def ensure_db_consistency():
349
+ urls = load_db_sqlite()
350
+ save_json(urls)
351
+
352
+ @app.before_request
353
+ def before_request_func():
354
+ if not hasattr(app, '_got_first_request'):
355
+ ensure_db_consistency()
356
+ app._got_first_request = True
357
+
358
+ if __name__ == '__main__':
359
+ print("Initializing database...")
360
+ init_db()
361
+ db_path = os.path.abspath(SQLITE_DB)
362
+ print(f"SQLite DB path: {db_path}")
363
+ if os.path.exists(db_path):
364
+ print(f"Database file exists, size: {os.path.getsize(db_path)} bytes")
365
+ else:
366
+ print("Warning: Database file does not exist after initialization!")
367
+
368
+ app.run(host='0.0.0.0', port=7860)