openfree commited on
Commit
3f2ee14
ยท
verified ยท
1 Parent(s): 22a2119

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1207
app.py DELETED
@@ -1,1207 +0,0 @@
1
- from flask import Flask, render_template_string, request, jsonify
2
- import os, re, json, sqlite3, logging
3
-
4
- app = Flask(__name__)
5
-
6
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. CONFIGURATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
7
- # Use absolute paths in the persistent directory for Hugging Face
8
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
9
- DB_FILE = os.path.join(BASE_DIR, "favorite_sites.json") # JSON file for backward compatibility
10
- SQLITE_DB = os.path.join(BASE_DIR, "favorite_sites.db") # SQLite database for persistence
11
- TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
12
-
13
- # Setup logging
14
- logging.basicConfig(level=logging.INFO)
15
- logger = logging.getLogger(__name__)
16
-
17
- # Domains that commonly block iframes
18
- BLOCKED_DOMAINS = [
19
- "naver.com", "daum.net", "google.com",
20
- "facebook.com", "instagram.com", "kakao.com",
21
- "ycombinator.com"
22
- ]
23
-
24
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 2. CURATED CATEGORIES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
25
- CATEGORIES = {
26
- "Free AI: Productivity": [
27
- "https://huggingface.co/spaces/ginigen/perflexity-clone",
28
- "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
29
- "https://huggingface.co/spaces/VIDraft/mouse-webgen",
30
- "https://huggingface.co/spaces/openfree/Vibe-Game",
31
- "https://huggingface.co/spaces/openfree/Game-Gallery",
32
- "https://huggingface.co/spaces/aiqtech/Contributors-Leaderboard",
33
- "https://huggingface.co/spaces/fantaxy/Model-Leaderboard",
34
- "https://huggingface.co/spaces/fantaxy/Space-Leaderboard",
35
- "https://huggingface.co/spaces/openfree/Korean-Leaderboard",
36
- ],
37
- "Free AI: Multimodal": [
38
- "https://huggingface.co/spaces/openfree/DreamO-video",
39
- "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
40
- "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
41
- "https://huggingface.co/spaces/fantaxy/Sound-AI-SFX",
42
- "https://huggingface.co/spaces/ginigen/SFX-Sound-magic",
43
- "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
44
- "https://huggingface.co/spaces/aiqcamp/MCP-kokoro",
45
- "https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
46
- ],
47
- "Free AI: Professional": [
48
- "https://huggingface.co/spaces/ginigen/blogger",
49
- "https://huggingface.co/spaces/VIDraft/money-radar",
50
- "https://huggingface.co/spaces/immunobiotech/drug-discovery",
51
- "https://huggingface.co/spaces/immunobiotech/Gemini-MICHELIN",
52
- "https://huggingface.co/spaces/Heartsync/Papers-Leaderboard",
53
- "https://huggingface.co/spaces/VIDraft/PapersImpact",
54
- "https://huggingface.co/spaces/ginipick/AgentX-Papers",
55
- "https://huggingface.co/spaces/openfree/Cycle-Navigator",
56
- ],
57
- "Free AI: Image": [
58
- "https://huggingface.co/spaces/ginigen/interior-design",
59
- "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
60
- "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
61
- "https://huggingface.co/spaces/ginigen/Every-Text",
62
- "https://huggingface.co/spaces/ginigen/text3d-r1",
63
- "https://huggingface.co/spaces/ginipick/FLUXllama",
64
- "https://huggingface.co/spaces/Heartsync/FLUX-Vision",
65
- "https://huggingface.co/spaces/ginigen/VisualCloze",
66
- "https://huggingface.co/spaces/seawolf2357/Ghibli-Multilingual-Text-rendering",
67
- "https://huggingface.co/spaces/ginigen/Ghibli-Meme-Studio",
68
- "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
69
- "https://huggingface.co/spaces/ginigen/3D-LLAMA",
70
- ],
71
- "Free AI: LLM / VLM": [
72
- "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
73
- "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
74
- "https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
75
- "https://huggingface.co/spaces/aiqcamp/gemini-2.5-flash-preview",
76
- "https://huggingface.co/spaces/openfree/qwen3-30b-a3b-research",
77
- "https://huggingface.co/spaces/openfree/qwen3-235b-a22b-research",
78
- "https://huggingface.co/spaces/openfree/Llama-4-Maverick-17B-Research",
79
- ],
80
- }
81
-
82
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. DATABASE FUNCTIONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
83
- def init_db():
84
- """Initialize both JSON and SQLite databases"""
85
- # JSON ํŒŒ์ผ ์ดˆ๊ธฐํ™”
86
- try:
87
- if not os.path.exists(DB_FILE):
88
- with open(DB_FILE, "w", encoding="utf-8") as f:
89
- json.dump([], f, ensure_ascii=False)
90
- logger.info("Created new JSON database file")
91
- except Exception as e:
92
- logger.error(f"Error creating JSON file: {e}")
93
-
94
- # SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”
95
- try:
96
- conn = sqlite3.connect(SQLITE_DB)
97
- cursor = conn.cursor()
98
- cursor.execute('''
99
- CREATE TABLE IF NOT EXISTS urls (
100
- id INTEGER PRIMARY KEY AUTOINCREMENT,
101
- url TEXT UNIQUE NOT NULL,
102
- date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP
103
- )
104
- ''')
105
- conn.commit()
106
-
107
- # JSON์—์„œ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
108
- json_urls = load_json()
109
- if json_urls:
110
- db_urls = load_db_sqlite()
111
- for url in json_urls:
112
- if url not in db_urls:
113
- try:
114
- cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
115
- except sqlite3.IntegrityError:
116
- pass # URL์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ
117
- conn.commit()
118
-
119
- conn.close()
120
- logger.info("SQLite database initialized successfully")
121
- except Exception as e:
122
- logger.error(f"Error initializing SQLite database: {e}")
123
-
124
-
125
-
126
- # Create default URLs file if none exists
127
- if not load_db():
128
- default_urls = [
129
- "https://huggingface.co/spaces/ginigen/perflexity-clone",
130
- "https://huggingface.co/spaces/openfree/Game-Gallery",
131
- "https://www.google.com"
132
- ]
133
- for url in default_urls:
134
- add_url_to_sqlite(url)
135
- save_json(default_urls)
136
- logger.info("Added default URLs to empty database")
137
-
138
- def load_json():
139
- """Load URLs from JSON file (for backward compatibility)"""
140
- try:
141
- if os.path.exists(DB_FILE):
142
- with open(DB_FILE, "r", encoding="utf-8") as f:
143
- raw = json.load(f)
144
- return raw if isinstance(raw, list) else []
145
- return []
146
- except Exception as e:
147
- logger.error(f"Error loading JSON file: {e}")
148
- return []
149
-
150
- def save_json(lst):
151
- """Save URLs to JSON file (for backward compatibility)"""
152
- try:
153
- with open(DB_FILE, "w", encoding="utf-8") as f:
154
- json.dump(lst, f, ensure_ascii=False, indent=2)
155
- logger.info(f"Saved {len(lst)} URLs to JSON file")
156
- return True
157
- except Exception as e:
158
- logger.error(f"Error saving to JSON file: {e}")
159
- return False
160
-
161
- def load_db_sqlite():
162
- """Load URLs from SQLite database"""
163
- try:
164
- conn = sqlite3.connect(SQLITE_DB)
165
- cursor = conn.cursor()
166
- cursor.execute("SELECT url FROM urls ORDER BY date_added DESC")
167
- urls = [row[0] for row in cursor.fetchall()]
168
- conn.close()
169
- logger.info(f"Loaded {len(urls)} URLs from SQLite database")
170
- return urls
171
- except Exception as e:
172
- logger.error(f"Error loading from SQLite database: {e}")
173
- return []
174
-
175
- def add_url_to_sqlite(url):
176
- """Add a URL to SQLite database"""
177
- try:
178
- conn = sqlite3.connect(SQLITE_DB)
179
- cursor = conn.cursor()
180
- cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
181
- conn.commit()
182
- conn.close()
183
- logger.info(f"Added URL to SQLite: {url}")
184
- return True
185
- except sqlite3.IntegrityError:
186
- # URL already exists
187
- logger.info(f"URL already exists in SQLite: {url}")
188
- return False
189
- except Exception as e:
190
- logger.error(f"Error adding URL to SQLite: {e}")
191
- return False
192
-
193
- def update_url_in_sqlite(old_url, new_url):
194
- """Update a URL in SQLite database"""
195
- try:
196
- conn = sqlite3.connect(SQLITE_DB)
197
- cursor = conn.cursor()
198
- cursor.execute("UPDATE urls SET url = ? WHERE url = ?", (new_url, old_url))
199
- if cursor.rowcount > 0:
200
- conn.commit()
201
- logger.info(f"Updated URL in SQLite: {old_url} -> {new_url}")
202
- success = True
203
- else:
204
- logger.info(f"URL not found for update in SQLite: {old_url}")
205
- success = False
206
- conn.close()
207
- return success
208
- except sqlite3.IntegrityError:
209
- # New URL already exists
210
- logger.error(f"New URL already exists in SQLite: {new_url}")
211
- return False
212
- except Exception as e:
213
- logger.error(f"Error updating URL in SQLite: {e}")
214
- return False
215
-
216
- def delete_url_from_sqlite(url):
217
- """Delete a URL from SQLite database"""
218
- try:
219
- conn = sqlite3.connect(SQLITE_DB)
220
- cursor = conn.cursor()
221
- cursor.execute("DELETE FROM urls WHERE url = ?", (url,))
222
- if cursor.rowcount > 0:
223
- conn.commit()
224
- logger.info(f"Deleted URL from SQLite: {url}")
225
- success = True
226
- else:
227
- logger.info(f"URL not found for deletion in SQLite: {url}")
228
- success = False
229
- conn.close()
230
- return success
231
- except Exception as e:
232
- logger.error(f"Error deleting URL from SQLite: {e}")
233
- return False
234
-
235
- def load_db():
236
- """Primary function to load URLs - prioritizes SQLite DB but falls back to JSON"""
237
- sqlite_urls = load_db_sqlite()
238
-
239
- # If SQLite DB is empty, try loading from JSON
240
- if not sqlite_urls:
241
- logger.info("SQLite database empty, trying JSON file")
242
- json_urls = load_json()
243
-
244
- # If we found URLs in JSON, migrate them to SQLite
245
- if json_urls:
246
- logger.info(f"Migrating {len(json_urls)} URLs from JSON to SQLite")
247
- for url in json_urls:
248
- add_url_to_sqlite(url)
249
- return json_urls
250
-
251
- return sqlite_urls
252
-
253
- def save_db(lst):
254
- """Save URLs to both SQLite and JSON"""
255
- logger.info(f"Saving {len(lst)} URLs to database")
256
-
257
- # Clear all URLs from SQLite and add the new list
258
- try:
259
- conn = sqlite3.connect(SQLITE_DB)
260
- cursor = conn.cursor()
261
- cursor.execute("DELETE FROM urls")
262
- for url in lst:
263
- cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
264
- conn.commit()
265
- conn.close()
266
- logger.info("Successfully saved to SQLite database")
267
- except Exception as e:
268
- logger.error(f"Error saving to SQLite database: {e}")
269
-
270
- # Also save to JSON for backward compatibility
271
- return save_json(lst)
272
-
273
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. URL HELPERS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
274
- def direct_url(hf_url):
275
- m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
276
- if not m:
277
- return hf_url
278
- owner, name = m.groups()
279
- owner = owner.lower()
280
- name = name.replace('.', '-').replace('_', '-').lower()
281
- return f"https://{owner}-{name}.hf.space"
282
-
283
- def screenshot_url(url):
284
- return f"https://image.thum.io/get/fullpage/{url}"
285
-
286
- def process_url_for_preview(url):
287
- """Returns (preview_url, mode)"""
288
- # Handle blocked domains first
289
- if any(d for d in BLOCKED_DOMAINS if d in url):
290
- return screenshot_url(url), "snapshot"
291
-
292
- # Special case handling for problematic URLs
293
- if "vibe-coding-tetris" in url or "World-of-Tank-GAME" in url or "Minesweeper-Game" in url:
294
- return screenshot_url(url), "snapshot"
295
-
296
- # General HF space handling
297
- try:
298
- if "huggingface.co/spaces" in url:
299
- parts = url.rstrip("/").split("/")
300
- if len(parts) >= 5:
301
- owner = parts[-2]
302
- name = parts[-1]
303
- embed_url = f"https://huggingface.co/spaces/{owner}/{name}/embed"
304
- return embed_url, "iframe"
305
- except Exception:
306
- return screenshot_url(url), "snapshot"
307
-
308
- # Default handling
309
- return url, "iframe"
310
-
311
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 5. API ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
312
- @app.route('/api/category')
313
- def api_category():
314
- cat = request.args.get('name', '')
315
- urls = CATEGORIES.get(cat, [])
316
- return jsonify([
317
- {
318
- "title": url.split('/')[-1],
319
- "owner": url.split('/')[-2] if '/spaces/' in url else '',
320
- "iframe": direct_url(url),
321
- "shot": screenshot_url(url),
322
- "hf": url
323
- } for url in urls
324
- ])
325
-
326
- @app.route('/api/favorites')
327
- def api_favorites():
328
- # Load URLs from SQLite database
329
- urls = load_db()
330
-
331
- page = int(request.args.get('page', 1))
332
- per_page = int(request.args.get('per_page', 9))
333
-
334
- total_pages = max(1, (len(urls) + per_page - 1) // per_page)
335
- start = (page - 1) * per_page
336
- end = min(start + per_page, len(urls))
337
-
338
- urls_page = urls[start:end]
339
-
340
- result = []
341
- for url in urls_page:
342
- try:
343
- preview_url, mode = process_url_for_preview(url)
344
- result.append({
345
- "title": url.split('/')[-1],
346
- "url": url,
347
- "preview_url": preview_url,
348
- "mode": mode
349
- })
350
- except Exception:
351
- # Fallback to screenshot mode
352
- result.append({
353
- "title": url.split('/')[-1],
354
- "url": url,
355
- "preview_url": screenshot_url(url),
356
- "mode": "snapshot"
357
- })
358
-
359
- return jsonify({
360
- "items": result,
361
- "page": page,
362
- "total_pages": total_pages
363
- })
364
-
365
- @app.route('/api/url/add', methods=['POST'])
366
- def add_url():
367
- url = request.form.get('url', '').strip()
368
- if not url:
369
- return jsonify({"success": False, "message": "URL is required"})
370
-
371
- # Check if URL already exists in database
372
- if not add_url_to_sqlite(url):
373
- return jsonify({"success": False, "message": "URL already exists"})
374
-
375
- # Also update JSON file for backward compatibility
376
- data = load_json()
377
- if url not in data:
378
- data.insert(0, url)
379
- save_json(data)
380
-
381
- return jsonify({"success": True, "message": "URL added successfully"})
382
-
383
- @app.route('/api/url/update', methods=['POST'])
384
- def update_url():
385
- old = request.form.get('old', '')
386
- new = request.form.get('new', '').strip()
387
-
388
- if not new:
389
- return jsonify({"success": False, "message": "New URL is required"})
390
-
391
- # Update in SQLite DB
392
- if not update_url_in_sqlite(old, new):
393
- return jsonify({"success": False, "message": "URL not found or new URL already exists"})
394
-
395
- # Also update JSON file for backward compatibility
396
- data = load_json()
397
- try:
398
- idx = data.index(old)
399
- data[idx] = new
400
- save_json(data)
401
- except ValueError:
402
- # If URL not in JSON, add it
403
- data.append(new)
404
- save_json(data)
405
-
406
- return jsonify({"success": True, "message": "URL updated successfully"})
407
-
408
- @app.route('/api/url/delete', methods=['POST'])
409
- def delete_url():
410
- url = request.form.get('url', '')
411
-
412
- # Delete from SQLite DB
413
- if not delete_url_from_sqlite(url):
414
- return jsonify({"success": False, "message": "URL not found"})
415
-
416
- # Also update JSON file for backward compatibility
417
- data = load_json()
418
- try:
419
- data.remove(url)
420
- save_json(data)
421
- except ValueError:
422
- pass
423
-
424
- return jsonify({"success": True, "message": "URL deleted successfully"})
425
-
426
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. STATIC ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
427
- @app.route('/static/<path:filename>')
428
- def serve_static(filename):
429
- static_dir = os.path.join(BASE_DIR, 'static')
430
- os.makedirs(static_dir, exist_ok=True)
431
- return send_from_directory(static_dir, filename)
432
-
433
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 7. HTML TEMPLATE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
434
- HTML_TEMPLATE = """<!DOCTYPE html>
435
- <html>
436
- <head>
437
- <meta charset="utf-8">
438
- <meta name="viewport" content="width=device-width, initial-scale=1">
439
- <title>AI Favorite Sites</title>
440
- <style>
441
- @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;600&display=swap');
442
- body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
443
- .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px;}
444
- .tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer;}
445
- .tab.active{background:#a78bfa;color:#1a202c;}
446
- .tab.manage{background:#ff6e91;color:white;}
447
- .tab.manage.active{background:#ff2d62;color:white;}
448
- .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;padding:0 16px 60px;}
449
- @media(max-width:800px){.grid{grid-template-columns:1fr;}}
450
- .card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:420px;display:flex;flex-direction:column;position:relative;}
451
- .frame{flex:1;position:relative;overflow:hidden;}
452
- .frame iframe{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0;}
453
- .frame img{width:100%;height:100%;object-fit:cover;}
454
- .card-label{position:absolute;top:10px;left:10px;padding:4px 8px;border-radius:4px;font-size:11px;font-weight:bold;z-index:100;text-transform:uppercase;letter-spacing:0.5px;box-shadow:0 2px 4px rgba(0,0,0,0.2);}
455
- .label-live{background:linear-gradient(135deg, #00c6ff, #0072ff);color:white;}
456
- .label-static{background:linear-gradient(135deg, #ff9a9e, #fad0c4);color:#333;}
457
- .foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee;}
458
- .foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none;}
459
- .pagination{display:flex;justify-content:center;margin:20px 0;gap:10px;}
460
- .pagination button{padding:5px 15px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;}
461
- .pagination button:disabled{opacity:0.5;cursor:not-allowed;}
462
- .manage-panel{background:white;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);margin:16px;padding:20px;}
463
- .form-group{margin-bottom:15px;}
464
- .form-group label{display:block;margin-bottom:5px;font-weight:600;}
465
- .form-control{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;}
466
- textarea.form-control{min-height:100px;font-family:monospace;resize:vertical;}
467
- .btn{padding:8px 15px;border:none;border-radius:4px;cursor:pointer;font-weight:600;}
468
- .btn-primary{background:#4a6dd8;color:white;}
469
- .btn-danger{background:#e53e3e;color:white;}
470
- .btn-success{background:#38a169;color:white;}
471
- .status{padding:10px;margin:10px 0;border-radius:4px;display:none;}
472
- .status.success{display:block;background:#c6f6d5;color:#22543d;}
473
- .status.error{display:block;background:#fed7d7;color:#822727;}
474
- .url-list{margin:20px 0;border:1px solid #eee;border-radius:4px;max-height:300px;overflow-y:auto;}
475
- .url-item{padding:10px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center;}
476
- .url-item:last-child{border-bottom:none;}
477
- .url-controls{display:flex;gap:5px;}
478
- </style>
479
- </head>
480
- <body>
481
- <header style="text-align: center; padding: 20px; background: linear-gradient(135deg, #f6f8fb, #e2e8f0); border-bottom: 1px solid #ddd;">
482
- <h1 style="margin-bottom: 10px;">๐ŸŒŸ AI Favorite Sites</h1>
483
- <p class="description" style="margin-bottom: 15px;">
484
- ๐Ÿš€ <strong>Free AI Spaces & Website Gallery</strong> โœจ Save and manage your favorite sites! Supports <span style="background: linear-gradient(135deg, #00c6ff, #0072ff); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">LIVE</span> and <span style="background: linear-gradient(135deg, #ff9a9e, #fad0c4); color: #333; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Static</span> preview modes.
485
- </p>
486
- <p>
487
- <a href="https://discord.gg/openfreeai" target="_blank"><img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="badge"></a>
488
- </p>
489
- </header>
490
- <div class="tabs" id="tabs"></div>
491
- <div id="content"></div>
492
-
493
- <script>
494
- // Basic configuration
495
- const cats = "${categories}";
496
- const tabs = document.getElementById('tabs');
497
- const content = document.getElementById('content');
498
- let active = "";
499
- let currentPage = 1;
500
-
501
- // LocalStorage functionality for URL persistence
502
- function saveToLocalStorage(urls) {
503
- try {
504
- localStorage.setItem('favoriteUrls', JSON.stringify(urls));
505
- console.log('Saved URLs to localStorage:', urls.length);
506
- return true;
507
- } catch (e) {
508
- console.error('Error saving to localStorage:', e);
509
- return false;
510
- }
511
- }
512
-
513
- function loadFromLocalStorage() {
514
- try {
515
- const data = localStorage.getItem('favoriteUrls');
516
- if (data) {
517
- const urls = JSON.parse(data);
518
- console.log('Loaded URLs from localStorage:', urls.length);
519
- return urls;
520
- }
521
- } catch (e) {
522
- console.error('Error loading from localStorage:', e);
523
- }
524
- return null;
525
- }
526
-
527
- // Export/Import functionality
528
- function exportUrls() {
529
- makeRequest('/api/favorites?per_page=1000', 'GET', null, function(data) {
530
- if (data.items && data.items.length > 0) {
531
- const urls = data.items.map(item => item.url);
532
-
533
- // Save to localStorage as backup
534
- saveToLocalStorage(urls);
535
-
536
- // Create file for download
537
- const blob = new Blob([JSON.stringify(urls, null, 2)], { type: 'application/json' });
538
- const a = document.createElement('a');
539
- a.href = URL.createObjectURL(blob);
540
- a.download = 'favorite_urls.json';
541
- document.body.appendChild(a);
542
- a.click();
543
- document.body.removeChild(a);
544
- } else {
545
- alert('No URLs to export');
546
- }
547
- });
548
- }
549
-
550
- function importUrls() {
551
- document.getElementById('import-file').click();
552
- }
553
-
554
- function handleImportFile(files) {
555
- if (files.length === 0) return;
556
-
557
- const file = files[0];
558
- const reader = new FileReader();
559
-
560
- reader.onload = function(e) {
561
- try {
562
- const urls = JSON.parse(e.target.result);
563
- if (Array.isArray(urls)) {
564
- // Save to localStorage
565
- saveToLocalStorage(urls);
566
-
567
- // Add each URL to the server
568
- let processed = 0;
569
-
570
- function addNextUrl(index) {
571
- if (index >= urls.length) {
572
- alert(`Import complete. Added ${processed} URLs.`);
573
- loadUrlList();
574
- if (active === 'Favorites') {
575
- loadFavorites(currentPage);
576
- }
577
- return;
578
- }
579
-
580
- const formData = new FormData();
581
- formData.append('url', urls[index]);
582
-
583
- makeRequest('/api/url/add', 'POST', formData, function(data) {
584
- if (data.success) processed++;
585
- addNextUrl(index + 1);
586
- });
587
- }
588
-
589
- addNextUrl(0);
590
- } else {
591
- alert('Invalid format. File must contain a JSON array of URLs.');
592
- }
593
- } catch (e) {
594
- alert('Error parsing file: ' + e.message);
595
- }
596
- };
597
-
598
- reader.readAsText(file);
599
- }
600
-
601
- // Simple utility functions
602
- function makeRequest(url, method, data, callback) {
603
- const xhr = new XMLHttpRequest();
604
- xhr.open(method, url, true);
605
- xhr.onreadystatechange = function() {
606
- if (xhr.readyState === 4 && xhr.status === 200) {
607
- callback(JSON.parse(xhr.responseText));
608
- }
609
- };
610
- if (method === 'POST') {
611
- xhr.send(data);
612
- } else {
613
- xhr.send();
614
- }
615
- }
616
-
617
- function updateTabs() {
618
- Array.from(tabs.children).forEach(b => {
619
- b.classList.toggle('active', b.dataset.c === active);
620
- });
621
- }
622
-
623
- // Tab handlers
624
- function loadCategory(cat) {
625
- if(cat === active) return;
626
- active = cat;
627
- updateTabs();
628
-
629
- content.innerHTML = '<p style="text-align:center;padding:40px">Loadingโ€ฆ</p>';
630
-
631
- makeRequest('/api/category?name=' + encodeURIComponent(cat), 'GET', null, function(data) {
632
- let html = '<div class="grid">';
633
-
634
- data.forEach(item => {
635
- html += `
636
- <div class="card">
637
- <div class="card-label label-live">LIVE</div>
638
- <div class="frame">
639
- <iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
640
- </div>
641
- <div class="foot">
642
- <a href="${item.hf}" target="_blank">${item.title}</a>
643
- </div>
644
- </div>
645
- `;
646
- });
647
-
648
- html += '</div>';
649
- content.innerHTML = html;
650
- });
651
- }
652
-
653
- function loadFavorites(page) {
654
- if(active === 'Favorites' && currentPage === page) return;
655
- active = 'Favorites';
656
- currentPage = page || 1;
657
- updateTabs();
658
-
659
- content.innerHTML = '<p style="text-align:center;padding:40px">Loadingโ€ฆ</p>';
660
-
661
- makeRequest('/api/favorites?page=' + currentPage, 'GET', null, function(data) {
662
- let html = '<div class="grid">';
663
-
664
- if(data.items.length === 0) {
665
- html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No favorites saved yet.</p>';
666
- } else {
667
- data.items.forEach(item => {
668
- if(item.mode === 'snapshot') {
669
- html += `
670
- <div class="card">
671
- <div class="card-label label-static">Static</div>
672
- <div class="frame">
673
- <img src="${item.preview_url}" loading="lazy">
674
- </div>
675
- <div class="foot">
676
- <a href="${item.url}" target="_blank">${item.title}</a>
677
- </div>
678
- </div>
679
- `;
680
- } else {
681
- html += `
682
- <div class="card">
683
- <div class="card-label label-live">LIVE</div>
684
- <div class="frame">
685
- <iframe src="${item.preview_url}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
686
- </div>
687
- <div class="foot">
688
- <a href="${item.url}" target="_blank">${item.title}</a>
689
- </div>
690
- </div>
691
- `;
692
- }
693
- });
694
- }
695
-
696
- html += '</div>';
697
-
698
- // Add pagination
699
- html += `
700
- <div class="pagination">
701
- <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadFavorites(${currentPage-1})">ยซ Previous</button>
702
- <span>Page ${currentPage} of ${data.total_pages}</span>
703
- <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadFavorites(${currentPage+1})">Next ยป</button>
704
- </div>
705
- `;
706
-
707
- content.innerHTML = html;
708
- });
709
- }
710
-
711
- function loadManage() {
712
- if(active === 'Manage') return;
713
- active = 'Manage';
714
- updateTabs();
715
-
716
- content.innerHTML = `
717
- <div class="manage-panel">
718
- <h2>Add New URL</h2>
719
- <div class="form-group">
720
- <label for="new-url">Single URL</label>
721
- <input type="text" id="new-url" class="form-control" placeholder="https://example.com">
722
- <button onclick="addUrl()" class="btn btn-primary" style="margin-top:10px">Add URL</button>
723
- </div>
724
-
725
- <div class="form-group" style="margin-top:20px">
726
- <label for="batch-urls">Multiple URLs (up to 100 URLs, one per line)</label>
727
- <textarea id="batch-urls" class="form-control" rows="8" placeholder="https://example1.com&#10;https://example2.com&#10;https://example3.com"></textarea>
728
- <button onclick="addBatchUrls()" class="btn btn-primary" style="margin-top:10px">Add All URLs</button>
729
- </div>
730
- <div id="add-status" class="status"></div>
731
- <div id="progress-bar" style="display:none; margin:15px 0;">
732
- <div style="height:20px; background-color:#f0f0f0; border-radius:4px; overflow:hidden;">
733
- <div id="progress-fill" style="height:100%; width:0%; background-color:#4a6dd8; transition:width 0.3s;"></div>
734
- </div>
735
- <div id="progress-text" style="text-align:center; margin-top:5px; font-size:14px;">0%</div>
736
- </div>
737
-
738
- <h2>Manage Saved URLs</h2>
739
- <div id="url-list" class="url-list">Loading...</div>
740
-
741
- <div style="margin-top: 30px;">
742
- <h3>Backup & Restore</h3>
743
- <p>Server storage may not persist across restarts. Use these options to save your data:</p>
744
- <button onclick="exportUrls()" class="btn btn-success">Export URLs</button>
745
- <button onclick="importUrls()" class="btn btn-primary">Import URLs</button>
746
- <input type="file" id="import-file" style="display:none" onchange="handleImportFile(this.files)">
747
- </div>
748
- </div>
749
- `;
750
-
751
- loadUrlList();
752
- }
753
-
754
- // URL management functions
755
- function loadUrlList() {
756
- // First try to load from localStorage as a fallback
757
- const localUrls = loadFromLocalStorage();
758
-
759
- makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
760
- const urlList = document.getElementById('url-list');
761
-
762
- // If server has no URLs but localStorage does, restore from localStorage
763
- if (data.items.length === 0 && localUrls && localUrls.length > 0) {
764
- showStatus('add-status', 'Restoring URLs from local backup...', true);
765
-
766
- // Add each URL from localStorage to the server
767
- let restored = 0;
768
-
769
- function restoreNextUrl(index) {
770
- if (index >= localUrls.length) {
771
- showStatus('add-status', `Restored ${restored} URLs from local backup`, true);
772
- // Reload the list after restoration
773
- setTimeout(() => {
774
- loadUrlList();
775
- if (active === 'Favorites') {
776
- loadFavorites(currentPage);
777
- }
778
- }, 1000);
779
- return;
780
- }
781
-
782
- const formData = new FormData();
783
- formData.append('url', localUrls[index]);
784
-
785
- makeRequest('/api/url/add', 'POST', formData, function(data) {
786
- if (data.success) restored++;
787
- restoreNextUrl(index + 1);
788
- });
789
- }
790
-
791
- restoreNextUrl(0);
792
- return;
793
- }
794
-
795
- if(data.items.length === 0) {
796
- urlList.innerHTML = '<p style="text-align:center;padding:20px">No URLs saved yet.</p>';
797
- return;
798
- }
799
-
800
- // Update localStorage with server data
801
- saveToLocalStorage(data.items.map(item => item.url));
802
-
803
- let html = '';
804
- data.items.forEach(item => {
805
- // Escape the URL to prevent JavaScript injection when used in onclick handlers
806
- const escapedUrl = item.url.replace(/'/g, "\\'");
807
-
808
- html += `
809
- <div class="url-item">
810
- <div>${item.url}</div>
811
- <div class="url-controls">
812
- <button class="btn" onclick="editUrl('${escapedUrl}')">Edit</button>
813
- <button class="btn btn-danger" onclick="deleteUrl('${escapedUrl}')">Delete</button>
814
- </div>
815
- </div>
816
- `;
817
- });
818
-
819
- urlList.innerHTML = html;
820
- });
821
- }
822
-
823
- function addUrl() {
824
- const url = document.getElementById('new-url').value.trim();
825
-
826
- if(!url) {
827
- showStatus('add-status', 'Please enter a URL', false);
828
- return;
829
- }
830
-
831
- const formData = new FormData();
832
- formData.append('url', url);
833
-
834
- makeRequest('/api/url/add', 'POST', formData, function(data) {
835
- showStatus('add-status', data.message, data.success);
836
- if(data.success) {
837
- document.getElementById('new-url').value = '';
838
-
839
- // Update localStorage
840
- const localUrls = loadFromLocalStorage() || [];
841
- if (!localUrls.includes(url)) {
842
- localUrls.unshift(url); // Add to beginning
843
- saveToLocalStorage(localUrls);
844
- }
845
-
846
- loadUrlList();
847
- // If currently in Favorites tab, reload to see changes immediately
848
- if(active === 'Favorites') {
849
- loadFavorites(currentPage);
850
- }
851
- }
852
- });
853
- }
854
-
855
- function addBatchUrls() {
856
- const textarea = document.getElementById('batch-urls');
857
- const text = textarea.value.trim();
858
-
859
- if (!text) {
860
- showStatus('add-status', 'Please enter at least one URL', false);
861
- return;
862
- }
863
-
864
- // Split by newlines and filter out empty lines
865
- let urls = text.split(/\r?\n/).filter(url => url.trim() !== '');
866
-
867
- // Limit to 100 URLs
868
- if (urls.length > 100) {
869
- showStatus('add-status', 'Too many URLs. Limited to 100 at once.', false);
870
- urls = urls.slice(0, 100);
871
- }
872
-
873
- if (urls.length === 0) {
874
- showStatus('add-status', 'No valid URLs found', false);
875
- return;
876
- }
877
-
878
- // Show progress bar
879
- const progressBar = document.getElementById('progress-bar');
880
- const progressFill = document.getElementById('progress-fill');
881
- const progressText = document.getElementById('progress-text');
882
- progressBar.style.display = 'block';
883
- progressFill.style.width = '0%';
884
- progressText.textContent = '0%';
885
-
886
- // Add URLs one by one
887
- let processed = 0;
888
- let succeeded = 0;
889
-
890
- function updateProgress() {
891
- const percentage = Math.round((processed / urls.length) * 100);
892
- progressFill.style.width = percentage + '%';
893
- progressText.textContent = `${processed}/${urls.length} (${percentage}%)`;
894
- }
895
-
896
- function addNextUrl(index) {
897
- if (index >= urls.length) {
898
- // All done
899
- setTimeout(() => {
900
- progressBar.style.display = 'none';
901
- textarea.value = '';
902
- showStatus('add-status', `Added ${succeeded} of ${urls.length} URLs successfully`, true);
903
-
904
- // Reload URL list and favorites
905
- loadUrlList();
906
- if (active === 'Favorites') {
907
- loadFavorites(currentPage);
908
- }
909
- }, 500);
910
- return;
911
- }
912
-
913
- const url = urls[index].trim();
914
- if (!url) {
915
- // Skip empty URLs
916
- processed++;
917
- updateProgress();
918
- addNextUrl(index + 1);
919
- return;
920
- }
921
-
922
- const formData = new FormData();
923
- formData.append('url', url);
924
-
925
- makeRequest('/api/url/add', 'POST', formData, function(data) {
926
- processed++;
927
-
928
- if (data.success) {
929
- succeeded++;
930
-
931
- // Update localStorage
932
- const localUrls = loadFromLocalStorage() || [];
933
- if (!localUrls.includes(url)) {
934
- localUrls.unshift(url);
935
- saveToLocalStorage(localUrls);
936
- }
937
- }
938
-
939
- updateProgress();
940
- addNextUrl(index + 1);
941
- });
942
- }
943
-
944
- // Start adding URLs
945
- addNextUrl(0);
946
- }
947
-
948
- function editUrl(url) {
949
- // Decode URL if it was previously escaped
950
- const decodedUrl = url.replace(/\\'/g, "'");
951
- const newUrl = prompt('Edit URL:', decodedUrl);
952
-
953
- if(!newUrl || newUrl === decodedUrl) return;
954
-
955
- const formData = new FormData();
956
- formData.append('old', decodedUrl);
957
- formData.append('new', newUrl);
958
-
959
- makeRequest('/api/url/update', 'POST', formData, function(data) {
960
- if(data.success) {
961
- // Update localStorage
962
- let localUrls = loadFromLocalStorage() || [];
963
- const index = localUrls.indexOf(decodedUrl);
964
- if (index !== -1) {
965
- localUrls[index] = newUrl;
966
- saveToLocalStorage(localUrls);
967
- }
968
-
969
- loadUrlList();
970
- // If currently in Favorites tab, reload to see changes immediately
971
- if(active === 'Favorites') {
972
- loadFavorites(currentPage);
973
- }
974
- } else {
975
- alert(data.message);
976
- }
977
- });
978
- }
979
-
980
- function deleteUrl(url) {
981
- // Decode URL if it was previously escaped
982
- const decodedUrl = url.replace(/\\'/g, "'");
983
- if(!confirm('Are you sure you want to delete this URL?')) return;
984
-
985
- const formData = new FormData();
986
- formData.append('url', decodedUrl);
987
-
988
- makeRequest('/api/url/delete', 'POST', formData, function(data) {
989
- if(data.success) {
990
- // Update localStorage
991
- let localUrls = loadFromLocalStorage() || [];
992
- const index = localUrls.indexOf(decodedUrl);
993
- if (index !== -1) {
994
- localUrls.splice(index, 1);
995
- saveToLocalStorage(localUrls);
996
- }
997
-
998
- loadUrlList();
999
- // If currently in Favorites tab, reload to see changes immediately
1000
- if(active === 'Favorites') {
1001
- loadFavorites(currentPage);
1002
- }
1003
- } else {
1004
- alert(data.message);
1005
- }
1006
- });
1007
- }
1008
-
1009
- function showStatus(id, message, success) {
1010
- const status = document.getElementById(id);
1011
- status.textContent = message;
1012
- status.className = success ? 'status success' : 'status error';
1013
- setTimeout(() => {
1014
- status.className = 'status';
1015
- }, 3000);
1016
- }
1017
-
1018
- // Create tabs
1019
- // Favorites tab first
1020
- const favTab = document.createElement('button');
1021
- favTab.className = 'tab';
1022
- favTab.textContent = 'Favorites';
1023
- favTab.dataset.c = 'Favorites';
1024
- favTab.onclick = function() { loadFavorites(1); };
1025
- tabs.appendChild(favTab);
1026
-
1027
- // Category tabs
1028
- cats.forEach(c => {
1029
- const b = document.createElement('button');
1030
- b.className = 'tab';
1031
- b.textContent = c;
1032
- b.dataset.c = c;
1033
- b.onclick = function() { loadCategory(c); };
1034
- tabs.appendChild(b);
1035
- });
1036
-
1037
- // Manage tab last
1038
- const manageTab = document.createElement('button');
1039
- manageTab.className = 'tab manage';
1040
- manageTab.textContent = 'Manage';
1041
- manageTab.dataset.c = 'Manage';
1042
- manageTab.onclick = function() { loadManage(); };
1043
- tabs.appendChild(manageTab);
1044
-
1045
- // Start with Favorites tab
1046
- loadFavorites(1);
1047
-
1048
- // Check for localStorage on page load
1049
- window.addEventListener('load', function() {
1050
- // Try to load URLs from localStorage
1051
- const localUrls = loadFromLocalStorage();
1052
-
1053
- // If we have URLs in localStorage, make sure they're on the server
1054
- if (localUrls && localUrls.length > 0) {
1055
- console.log(`Found ${localUrls.length} URLs in localStorage`);
1056
-
1057
- // First get server URLs to compare
1058
- makeRequest('/api/favorites?per_page=1000', 'GET', null, function(data) {
1059
- const serverUrls = data.items.map(item => item.url);
1060
-
1061
- // Find URLs that are in localStorage but not on server
1062
- const missingUrls = localUrls.filter(url => !serverUrls.includes(url));
1063
-
1064
- if (missingUrls.length > 0) {
1065
- console.log(`Found ${missingUrls.length} URLs missing from server, restoring...`);
1066
-
1067
- // Add missing URLs to server
1068
- let restored = 0;
1069
-
1070
- function restoreNextUrl(index) {
1071
- if (index >= missingUrls.length) {
1072
- console.log(`Restored ${restored} URLs from localStorage`);
1073
- if (active === 'Favorites') {
1074
- loadFavorites(currentPage);
1075
- }
1076
- return;
1077
- }
1078
-
1079
- const formData = new FormData();
1080
- formData.append('url', missingUrls[index]);
1081
-
1082
- makeRequest('/api/url/add', 'POST', formData, function(data) {
1083
- if (data.success) restored++;
1084
- restoreNextUrl(index + 1);
1085
- });
1086
- }
1087
-
1088
- restoreNextUrl(0);
1089
- }
1090
- });
1091
- }
1092
- });
1093
- </script>
1094
- </body>
1095
- </html>
1096
- """
1097
-
1098
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 8. MAIN ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1099
- @app.route('/')
1100
- def home():
1101
- # ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก์„ JSON์œผ๋กœ ๋ณ€ํ™˜
1102
- categories_json = json.dumps(list(CATEGORIES.keys()))
1103
-
1104
- # ๋””๋ฒ„๊น…์„ ์œ„ํ•ด ๋กœ๊ทธ ์ถ”๊ฐ€
1105
- logger.info(f"Categories JSON: {categories_json}")
1106
-
1107
- # HTML์—์„œ cats ๋ณ€์ˆ˜๊ฐ€ JavaScript ๋ฐฐ์—ด๋กœ ์ธ์‹๋˜๋„๋ก ์ˆ˜์ •
1108
- html_content = HTML_TEMPLATE.replace('const cats = "${categories}";', f'const cats = {categories_json};')
1109
-
1110
- return html_content
1111
-
1112
- @app.route('/debug')
1113
- def debug():
1114
- """๋””๋ฒ„๊น… ํŽ˜์ด์ง€"""
1115
- categories_json = json.dumps(list(CATEGORIES.keys()))
1116
- return f"""
1117
- <!DOCTYPE html>
1118
- <html>
1119
- <head>
1120
- <title>๋””๋ฒ„๊ทธ ํŽ˜์ด์ง€</title>
1121
- </head>
1122
- <body>
1123
- <h1>์นดํ…Œ๊ณ ๋ฆฌ ํ…Œ์ŠคํŠธ</h1>
1124
- <div id="result"></div>
1125
-
1126
- <script>
1127
- // ์นดํ…Œ๊ณ ๋ฆฌ ์ง์ ‘ ํ• ๋‹น
1128
- const cats = {categories_json};
1129
-
1130
- document.getElementById('result').innerHTML =
1131
- "<p>Categories: " + JSON.stringify(cats) + "</p>" +
1132
- "<p>Type: " + typeof cats + "</p>" +
1133
- "<p>Array? " + Array.isArray(cats) + "</p>" +
1134
- "<p>Length: " + cats.length + "</p>";
1135
-
1136
- // ๊ฐ„๋‹จํ•œ ํƒญ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ
1137
- const tabsDiv = document.createElement('div');
1138
- tabsDiv.style.display = 'flex';
1139
- tabsDiv.style.gap = '10px';
1140
- document.body.appendChild(tabsDiv);
1141
-
1142
- cats.forEach(cat => {{
1143
- const btn = document.createElement('button');
1144
- btn.textContent = cat;
1145
- btn.style.padding = '5px 15px';
1146
- tabsDiv.appendChild(btn);
1147
- }});
1148
- </script>
1149
- </body>
1150
- </html>
1151
- """
1152
-
1153
-
1154
- # Initialize database on startup
1155
- init_db()
1156
-
1157
- # Define a function to ensure database consistency
1158
- def ensure_db_consistency():
1159
- """Make sure both databases are in sync"""
1160
- try:
1161
- # Get URLs from both sources
1162
- sqlite_urls = load_db_sqlite()
1163
- json_urls = load_json()
1164
-
1165
- # Combine and deduplicate
1166
- all_urls = list(set(sqlite_urls + json_urls))
1167
-
1168
- # If there are differences, update both databases
1169
- if len(all_urls) != len(sqlite_urls) or len(all_urls) != len(json_urls):
1170
- logger.info("Database inconsistency detected, synchronizing...")
1171
- # Save to both databases
1172
- save_db(all_urls)
1173
-
1174
- # Double-check if save was successful
1175
- sqlite_check = load_db_sqlite()
1176
- json_check = load_json()
1177
-
1178
- if len(sqlite_check) != len(all_urls) or len(json_check) != len(all_urls):
1179
- logger.error(f"Database synchronization failed! SQLite: {len(sqlite_check)}, JSON: {len(json_check)}, Expected: {len(all_urls)}")
1180
- else:
1181
- logger.info("Database synchronization successful")
1182
- except Exception as e:
1183
- logger.error(f"Error during database consistency check: {e}")
1184
-
1185
- # For Flask 2.0+ compatibility
1186
- @app.before_request
1187
- def before_request_func():
1188
- # Use a flag to run this only once
1189
- if not hasattr(app, '_got_first_request'):
1190
- ensure_db_consistency()
1191
- app._got_first_request = True
1192
-
1193
- # Log database status
1194
- logger.info(f"Database status - SQLite: {len(load_db_sqlite())} URLs, JSON: {len(load_json())} URLs")
1195
-
1196
- if __name__ == '__main__':
1197
- # ๋ช…์‹œ์ ์œผ๋กœ DB ์ดˆ๊ธฐํ™”
1198
- logger.info("Initializing database...")
1199
- init_db()
1200
-
1201
- # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ ์ƒํƒœ ํ™•์ธ
1202
- if os.path.exists(SQLITE_DB):
1203
- logger.info(f"Database file exists, size: {os.path.getsize(SQLITE_DB)} bytes")
1204
- else:
1205
- logger.error("Database file does not exist after initialization!")
1206
-
1207
- app.run(host='0.0.0.0', port=7860, debug=True) # debug=True ์ถ”๊ฐ€๋กœ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ™•์ธ