fantaxy commited on
Commit
829df6e
Β·
verified Β·
1 Parent(s): 8d0d7fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -530
app.py CHANGED
@@ -1,11 +1,9 @@
1
  from flask import Flask, render_template, request, jsonify
2
- import os, re, json, sqlite3
3
 
4
  app = Flask(__name__)
5
 
6
  # ────────────────────────── 1. CONFIGURATION ──────────────────────────
7
- DB_FILE = "favorite_sites.json" # JSON file for backward compatibility
8
- SQLITE_DB = "favorite_sites.db" # SQLite database for persistence
9
 
10
  # Domains that commonly block iframes
11
  BLOCKED_DOMAINS = [
@@ -14,15 +12,193 @@ BLOCKED_DOMAINS = [
14
  "ycombinator.com"
15
  ]
16
 
17
-
18
  # ────────────────────────── 2. CURATED CATEGORIES ──────────────────────────
19
  CATEGORIES = {
20
-
21
- "Productivity": [
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  "https://huggingface.co/spaces/aiqtech/Heatmap-Leaderboard",
24
  "https://huggingface.co/spaces/VIDraft/DNA-CASINO",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
 
 
 
 
26
  "https://huggingface.co/spaces/openfree/Open-GAMMA",
27
  "https://huggingface.co/spaces/VIDraft/Robo-Beam",
28
  "https://huggingface.co/spaces/VIDraft/voice-trans",
@@ -34,7 +210,6 @@ CATEGORIES = {
34
  "https://huggingface.co/spaces/ginigen/perflexity-clone",
35
  "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
36
  "https://huggingface.co/spaces/ginipick/10m-marketing",
37
-
38
  "https://huggingface.co/spaces/openfree/Live-Podcast",
39
  "https://huggingface.co/spaces/openfree/AI-Podcast",
40
  "https://huggingface.co/spaces/ginipick/QR-Canvas-plus",
@@ -47,9 +222,7 @@ CATEGORIES = {
47
  "https://huggingface.co/spaces/ginipick/Change-Hair",
48
  ],
49
  "Multimodal": [
50
-
51
  "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
52
-
53
  "https://huggingface.co/spaces/fantaxy/YTB-TEST",
54
  "https://huggingface.co/spaces/ginigen/Seedance-Free",
55
  "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
@@ -121,7 +294,6 @@ CATEGORIES = {
121
  "https://huggingface.co/spaces/openfree/VectorFlow",
122
  "https://huggingface.co/spaces/ginigen/3D-LLAMA",
123
  "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
124
-
125
  ],
126
  "LLM / VLM": [
127
  "https://huggingface.co/spaces/fantaxy/fantasy-novel",
@@ -142,136 +314,7 @@ CATEGORIES = {
142
  ],
143
  }
144
 
145
- # ────────────────────────── 3. DATABASE FUNCTIONS ──────────────────────────
146
- def init_db():
147
- # Initialize JSON file if it doesn't exist
148
- if not os.path.exists(DB_FILE):
149
- with open(DB_FILE, "w", encoding="utf-8") as f:
150
- json.dump([], f, ensure_ascii=False)
151
-
152
- # Initialize SQLite database
153
- conn = sqlite3.connect(SQLITE_DB)
154
- cursor = conn.cursor()
155
- cursor.execute('''
156
- CREATE TABLE IF NOT EXISTS urls (
157
- id INTEGER PRIMARY KEY AUTOINCREMENT,
158
- url TEXT UNIQUE NOT NULL,
159
- date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP
160
- )
161
- ''')
162
- conn.commit()
163
-
164
- # If we have data in JSON but not in SQLite (first run with new SQLite DB),
165
- # migrate the data from JSON to SQLite
166
- json_urls = load_json()
167
- if json_urls:
168
- db_urls = load_db_sqlite()
169
- for url in json_urls:
170
- if url not in db_urls:
171
- add_url_to_sqlite(url)
172
-
173
- conn.close()
174
-
175
- def load_json():
176
- """Load URLs from JSON file (for backward compatibility)"""
177
- try:
178
- with open(DB_FILE, "r", encoding="utf-8") as f:
179
- raw = json.load(f)
180
- return raw if isinstance(raw, list) else []
181
- except Exception:
182
- return []
183
-
184
- def save_json(lst):
185
- """Save URLs to JSON file (for backward compatibility)"""
186
- try:
187
- with open(DB_FILE, "w", encoding="utf-8") as f:
188
- json.dump(lst, f, ensure_ascii=False, indent=2)
189
- return True
190
- except Exception:
191
- return False
192
-
193
- def load_db_sqlite():
194
- """Load URLs from SQLite database"""
195
- conn = sqlite3.connect(SQLITE_DB)
196
- cursor = conn.cursor()
197
- cursor.execute("SELECT url FROM urls ORDER BY date_added DESC")
198
- urls = [row[0] for row in cursor.fetchall()]
199
- conn.close()
200
- return urls
201
-
202
- def add_url_to_sqlite(url):
203
- """Add a URL to SQLite database"""
204
- conn = sqlite3.connect(SQLITE_DB)
205
- cursor = conn.cursor()
206
- try:
207
- cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
208
- conn.commit()
209
- success = True
210
- except sqlite3.IntegrityError:
211
- # URL already exists
212
- success = False
213
- conn.close()
214
- return success
215
-
216
- def update_url_in_sqlite(old_url, new_url):
217
- """Update a URL in SQLite database"""
218
- conn = sqlite3.connect(SQLITE_DB)
219
- cursor = conn.cursor()
220
- try:
221
- cursor.execute("UPDATE urls SET url = ? WHERE url = ?", (new_url, old_url))
222
- if cursor.rowcount > 0:
223
- conn.commit()
224
- success = True
225
- else:
226
- success = False
227
- except sqlite3.IntegrityError:
228
- # New URL already exists
229
- success = False
230
- conn.close()
231
- return success
232
-
233
- def delete_url_from_sqlite(url):
234
- """Delete a URL from SQLite database"""
235
- conn = sqlite3.connect(SQLITE_DB)
236
- cursor = conn.cursor()
237
- cursor.execute("DELETE FROM urls WHERE url = ?", (url,))
238
- if cursor.rowcount > 0:
239
- conn.commit()
240
- success = True
241
- else:
242
- success = False
243
- conn.close()
244
- return success
245
-
246
- def load_db():
247
- """Primary function to load URLs - prioritizes SQLite DB but falls back to JSON"""
248
- urls = load_db_sqlite()
249
- if not urls:
250
- # If SQLite DB is empty, try loading from JSON
251
- urls = load_json()
252
- # If we found URLs in JSON, migrate them to SQLite
253
- for url in urls:
254
- add_url_to_sqlite(url)
255
- return urls
256
-
257
- def save_db(lst):
258
- """Save URLs to both SQLite and JSON"""
259
- # Get existing URLs from SQLite for comparison
260
- existing_urls = load_db_sqlite()
261
-
262
- # Clear all URLs from SQLite and add the new list
263
- conn = sqlite3.connect(SQLITE_DB)
264
- cursor = conn.cursor()
265
- cursor.execute("DELETE FROM urls")
266
- for url in lst:
267
- cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
268
- conn.commit()
269
- conn.close()
270
-
271
- # Also save to JSON for backward compatibility
272
- return save_json(lst)
273
-
274
- # ────────────────────────── 4. URL HELPERS ──────────────────────────
275
  def direct_url(hf_url):
276
  m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
277
  if not m:
@@ -309,15 +352,15 @@ def process_url_for_preview(url):
309
  # Default handling
310
  return url, "iframe"
311
 
312
- # ────────────────────────── 5. API ROUTES ──────────────────────────
313
  @app.route('/api/category')
314
  def api_category():
315
  cat = request.args.get('name', '')
316
  urls = CATEGORIES.get(cat, [])
317
 
318
- # Add pagination for categories as well
319
  page = int(request.args.get('page', 1))
320
- per_page = int(request.args.get('per_page', 4)) # Changed to 4 per page
321
 
322
  total_pages = max(1, (len(urls) + per_page - 1) // per_page)
323
  start = (page - 1) * per_page
@@ -341,122 +384,7 @@ def api_category():
341
  "total_pages": total_pages
342
  })
343
 
344
- @app.route('/api/favorites')
345
- def api_favorites():
346
- # Load URLs from SQLite database
347
- urls = load_db()
348
-
349
- page = int(request.args.get('page', 1))
350
- per_page = int(request.args.get('per_page', 4)) # Changed to 4 per page
351
-
352
- total_pages = max(1, (len(urls) + per_page - 1) // per_page)
353
- start = (page - 1) * per_page
354
- end = min(start + per_page, len(urls))
355
-
356
- urls_page = urls[start:end]
357
-
358
- result = []
359
- for url in urls_page:
360
- try:
361
- preview_url, mode = process_url_for_preview(url)
362
- result.append({
363
- "title": url.split('/')[-1],
364
- "url": url,
365
- "preview_url": preview_url,
366
- "mode": mode
367
- })
368
- except Exception:
369
- # Fallback to screenshot mode
370
- result.append({
371
- "title": url.split('/')[-1],
372
- "url": url,
373
- "preview_url": screenshot_url(url),
374
- "mode": "snapshot"
375
- })
376
-
377
- return jsonify({
378
- "items": result,
379
- "page": page,
380
- "total_pages": total_pages
381
- })
382
-
383
- @app.route('/api/url/add', methods=['POST'])
384
- def add_url():
385
- url = request.form.get('url', '').strip()
386
- if not url:
387
- return jsonify({"success": False, "message": "URL is required"})
388
-
389
- # SQLite에 μΆ”κ°€ μ‹œλ„
390
- conn = sqlite3.connect(SQLITE_DB)
391
- cursor = conn.cursor()
392
- try:
393
- cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
394
- conn.commit()
395
- success = True
396
- except sqlite3.IntegrityError:
397
- # URL이 이미 μ‘΄μž¬ν•˜λŠ” 경우
398
- success = False
399
- except Exception as e:
400
- print(f"SQLite error: {str(e)}")
401
- success = False
402
- finally:
403
- conn.close()
404
-
405
- if not success:
406
- return jsonify({"success": False, "message": "URL already exists or could not be added"})
407
-
408
- # JSON νŒŒμΌμ—λ„ μΆ”κ°€ (λ°±μ—…μš©)
409
- data = load_json()
410
- if url not in data:
411
- data.insert(0, url)
412
- save_json(data)
413
-
414
- return jsonify({"success": True, "message": "URL added successfully"})
415
-
416
- @app.route('/api/url/update', methods=['POST'])
417
- def update_url():
418
- old = request.form.get('old', '')
419
- new = request.form.get('new', '').strip()
420
-
421
- if not new:
422
- return jsonify({"success": False, "message": "New URL is required"})
423
-
424
- # Update in SQLite DB
425
- if not update_url_in_sqlite(old, new):
426
- return jsonify({"success": False, "message": "URL not found or new URL already exists"})
427
-
428
- # Also update JSON file for backward compatibility
429
- data = load_json()
430
- try:
431
- idx = data.index(old)
432
- data[idx] = new
433
- save_json(data)
434
- except ValueError:
435
- # If URL not in JSON, add it
436
- data.append(new)
437
- save_json(data)
438
-
439
- return jsonify({"success": True, "message": "URL updated successfully"})
440
-
441
- @app.route('/api/url/delete', methods=['POST'])
442
- def delete_url():
443
- url = request.form.get('url', '')
444
-
445
- # Delete from SQLite DB
446
- if not delete_url_from_sqlite(url):
447
- return jsonify({"success": False, "message": "URL not found"})
448
-
449
- # Also update JSON file for backward compatibility
450
- data = load_json()
451
- try:
452
- data.remove(url)
453
- save_json(data)
454
- except ValueError:
455
- pass
456
-
457
- return jsonify({"success": True, "message": "URL deleted successfully"})
458
-
459
- # ────────────────────────── 6. MAIN ROUTES ──────────────────────────
460
  @app.route('/')
461
  def home():
462
  os.makedirs('templates', exist_ok=True)
@@ -474,8 +402,12 @@ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
474
  .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px;}
475
  .tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer;}
476
  .tab.active{background:#a78bfa;color:#1a202c;}
477
- .tab.manage{background:#ff6e91;color:white;}
478
- .tab.manage.active{background:#ff2d62;color:white;}
 
 
 
 
479
  /* Updated grid to show 2x2 layout */
480
  .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;padding:0 16px 60px;max-width:1200px;margin:0 auto;}
481
  @media(max-width:800px){.grid{grid-template-columns:1fr;}}
@@ -492,29 +424,16 @@ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
492
  .pagination{display:flex;justify-content:center;margin:20px 0;gap:10px;}
493
  .pagination button{padding:5px 15px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;}
494
  .pagination button:disabled{opacity:0.5;cursor:not-allowed;}
495
- .manage-panel{background:white;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);margin:16px;padding:20px;}
496
- .form-group{margin-bottom:15px;}
497
- .form-group label{display:block;margin-bottom:5px;font-weight:600;}
498
- .form-control{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;}
499
- .btn{padding:8px 15px;border:none;border-radius:4px;cursor:pointer;font-weight:600;}
500
- .btn-primary{background:#4a6dd8;color:white;}
501
- .btn-danger{background:#e53e3e;color:white;}
502
- .btn-success{background:#38a169;color:white;}
503
- .status{padding:10px;margin:10px 0;border-radius:4px;display:none;}
504
- .status.success{display:block;background:#c6f6d5;color:#22543d;}
505
- .status.error{display:block;background:#fed7d7;color:#822727;}
506
- .url-list{margin:20px 0;border:1px solid #eee;border-radius:4px;max-height:300px;overflow-y:auto;}
507
- .url-item{padding:10px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center;}
508
- .url-item:last-child{border-bottom:none;}
509
- .url-controls{display:flex;gap:5px;}
510
  </style>
511
  </head>
512
  <body>
513
  <header style="text-align: center; padding: 20px; background: linear-gradient(135deg, #f6f8fb, #e2e8f0); border-bottom: 1px solid #ddd;">
514
- <h1 style="margin-bottom: 10px;">🌟AI Playground</h1>
515
  <p>
516
  <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>
517
- </p>
 
 
518
  </header>
519
  <div class="tabs" id="tabs"></div>
520
  <div id="content"></div>
@@ -528,17 +447,6 @@ let active = "";
528
  let currentPage = 1;
529
 
530
  // Simple utility functions
531
- function loadHTML(url, callback) {
532
- const xhr = new XMLHttpRequest();
533
- xhr.open('GET', url, true);
534
- xhr.onreadystatechange = function() {
535
- if (xhr.readyState === 4 && xhr.status === 200) {
536
- callback(xhr.responseText);
537
- }
538
- };
539
- xhr.send();
540
- }
541
-
542
  function makeRequest(url, method, data, callback) {
543
  const xhr = new XMLHttpRequest();
544
  xhr.open(method, url, true);
@@ -605,224 +513,31 @@ function loadCategory(cat, page) {
605
  });
606
  }
607
 
608
- function loadFavorites(page) {
609
- if(active === 'Favorites' && currentPage === page) return;
610
- active = 'Favorites';
611
- currentPage = page || 1;
612
- updateTabs();
613
-
614
- content.innerHTML = '<p style="text-align:center;padding:40px">Loading…</p>';
615
-
616
- makeRequest('/api/favorites?page=' + currentPage + '&per_page=4', 'GET', null, function(data) {
617
- let html = '<div class="grid">';
618
-
619
- if(data.items.length === 0) {
620
- html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No favorites saved yet.</p>';
621
- } else {
622
- data.items.forEach(item => {
623
- if(item.mode === 'snapshot') {
624
- html += `
625
- <div class="card">
626
- <div class="card-label label-static">Static</div>
627
- <div class="frame">
628
- <img src="${item.preview_url}" loading="lazy">
629
- </div>
630
- <div class="foot">
631
- <a href="${item.url}" target="_blank">${item.title}</a>
632
- </div>
633
- </div>
634
- `;
635
- } else {
636
- html += `
637
- <div class="card">
638
- <div class="card-label label-live">LIVE</div>
639
- <div class="frame">
640
- <iframe src="${item.preview_url}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
641
- </div>
642
- <div class="foot">
643
- <a href="${item.url}" target="_blank">${item.title}</a>
644
- </div>
645
- </div>
646
- `;
647
- }
648
- });
649
- }
650
-
651
- html += '</div>';
652
-
653
- // Add pagination
654
- html += `
655
- <div class="pagination">
656
- <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadFavorites(${currentPage-1})">Β« Previous</button>
657
- <span>Page ${currentPage} of ${data.total_pages}</span>
658
- <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadFavorites(${currentPage+1})">Next Β»</button>
659
- </div>
660
- `;
661
-
662
- content.innerHTML = html;
663
- });
664
- }
665
-
666
- function loadManage() {
667
- if(active === 'Manage') return;
668
- active = 'Manage';
669
- updateTabs();
670
-
671
- content.innerHTML = `
672
- <div class="manage-panel">
673
- <h2>Add New URL</h2>
674
- <div class="form-group">
675
- <label for="new-url">URL</label>
676
- <input type="text" id="new-url" class="form-control" placeholder="https://example.com">
677
- </div>
678
- <button onclick="addUrl()" class="btn btn-primary">Add URL</button>
679
- <div id="add-status" class="status"></div>
680
-
681
- <h2>Manage Saved URLs</h2>
682
- <div id="url-list" class="url-list">Loading...</div>
683
- </div>
684
- `;
685
-
686
- loadUrlList();
687
- }
688
-
689
- // URL management functions
690
- function loadUrlList() {
691
- makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
692
- const urlList = document.getElementById('url-list');
693
-
694
- if(data.items.length === 0) {
695
- urlList.innerHTML = '<p style="text-align:center;padding:20px">No URLs saved yet.</p>';
696
- return;
697
- }
698
-
699
- let html = '';
700
- data.items.forEach(item => {
701
- // Escape the URL to prevent JavaScript injection when used in onclick handlers
702
- const escapedUrl = item.url.replace(/'/g, "\\'");
703
-
704
- html += `
705
- <div class="url-item">
706
- <div>${item.url}</div>
707
- <div class="url-controls">
708
- <button class="btn" onclick="editUrl('${escapedUrl}')">Edit</button>
709
- <button class="btn btn-danger" onclick="deleteUrl('${escapedUrl}')">Delete</button>
710
- </div>
711
- </div>
712
- `;
713
- });
714
-
715
- urlList.innerHTML = html;
716
- });
717
- }
718
-
719
- function addUrl() {
720
- const url = document.getElementById('new-url').value.trim();
721
-
722
- if(!url) {
723
- showStatus('add-status', 'Please enter a URL', false);
724
- return;
725
- }
726
-
727
- const formData = new FormData();
728
- formData.append('url', url);
729
-
730
- makeRequest('/api/url/add', 'POST', formData, function(data) {
731
- showStatus('add-status', data.message, data.success);
732
- if(data.success) {
733
- document.getElementById('new-url').value = '';
734
- loadUrlList();
735
- // If currently in Favorites tab, reload to see changes immediately
736
- if(active === 'Favorites') {
737
- loadFavorites(currentPage);
738
- }
739
- }
740
- });
741
- }
742
-
743
- function editUrl(url) {
744
- // Decode URL if it was previously escaped
745
- const decodedUrl = url.replace(/\\'/g, "'");
746
- const newUrl = prompt('Edit URL:', decodedUrl);
747
-
748
- if(!newUrl || newUrl === decodedUrl) return;
749
-
750
- const formData = new FormData();
751
- formData.append('old', decodedUrl);
752
- formData.append('new', newUrl);
753
-
754
- makeRequest('/api/url/update', 'POST', formData, function(data) {
755
- if(data.success) {
756
- loadUrlList();
757
- // If currently in Favorites tab, reload to see changes immediately
758
- if(active === 'Favorites') {
759
- loadFavorites(currentPage);
760
- }
761
- } else {
762
- alert(data.message);
763
- }
764
- });
765
- }
766
-
767
- function deleteUrl(url) {
768
- // Decode URL if it was previously escaped
769
- const decodedUrl = url.replace(/\\'/g, "'");
770
- if(!confirm('Are you sure you want to delete this URL?')) return;
771
-
772
- const formData = new FormData();
773
- formData.append('url', decodedUrl);
774
-
775
- makeRequest('/api/url/delete', 'POST', formData, function(data) {
776
- if(data.success) {
777
- loadUrlList();
778
- // If currently in Favorites tab, reload to see changes immediately
779
- if(active === 'Favorites') {
780
- loadFavorites(currentPage);
781
- }
782
- } else {
783
- alert(data.message);
784
- }
785
- });
786
- }
787
-
788
- function showStatus(id, message, success) {
789
- const status = document.getElementById(id);
790
- status.textContent = message;
791
- status.className = success ? 'status success' : 'status error';
792
- setTimeout(() => {
793
- status.className = 'status';
794
- }, 3000);
795
- }
796
-
797
  // Create tabs
798
- // Favorites tab first
799
- const favTab = document.createElement('button');
800
- favTab.className = 'tab';
801
- favTab.textContent = 'Favorites';
802
- favTab.dataset.c = 'Favorites';
803
- favTab.onclick = function() { loadFavorites(1); };
804
- tabs.appendChild(favTab);
805
-
806
- // Category tabs
807
- cats.forEach(c => {
808
  const b = document.createElement('button');
809
- b.className = 'tab';
810
- b.textContent = c;
811
- b.dataset.c = c;
812
- b.onclick = function() { loadCategory(c, 1); };
813
  tabs.appendChild(b);
814
  });
815
 
816
- // Manage tab last
817
- const manageTab = document.createElement('button');
818
- manageTab.className = 'tab manage';
819
- manageTab.textContent = 'Manage';
820
- manageTab.dataset.c = 'Manage';
821
- manageTab.onclick = function() { loadManage(); };
822
- tabs.appendChild(manageTab);
 
 
 
 
823
 
824
- // Start with Favorites tab
825
- loadFavorites(1);
826
  </script>
827
  </body>
828
  </html>''')
@@ -830,34 +545,5 @@ loadFavorites(1);
830
  # Return the rendered template
831
  return render_template('index.html', cats=list(CATEGORIES.keys()))
832
 
833
- # Initialize database on startup
834
- init_db()
835
-
836
- # Define a function to ensure database consistency
837
- def ensure_db_consistency():
838
- # Make sure we have the latest data in both JSON and SQLite
839
- urls = load_db_sqlite()
840
- save_json(urls)
841
-
842
- # For Flask 2.0+ compatibility
843
- @app.before_request
844
- def before_request_func():
845
- # Use a flag to run this only once
846
- if not hasattr(app, '_got_first_request'):
847
- ensure_db_consistency()
848
- app._got_first_request = True
849
-
850
  if __name__ == '__main__':
851
- # μ•± μ‹œμž‘ 전에 λͺ…μ‹œμ μœΌλ‘œ DB μ΄ˆκΈ°ν™”
852
- print("Initializing database...")
853
- init_db()
854
-
855
- # λ°μ΄ν„°λ² μ΄μŠ€ 파일 경둜 및 쑴재 μ—¬λΆ€ 확인
856
- db_path = os.path.abspath(SQLITE_DB)
857
- print(f"SQLite DB path: {db_path}")
858
- if os.path.exists(SQLITE_DB):
859
- print(f"Database file exists, size: {os.path.getsize(SQLITE_DB)} bytes")
860
- else:
861
- print("Warning: Database file does not exist after initialization!")
862
-
863
  app.run(host='0.0.0.0', port=7860)
 
1
  from flask import Flask, render_template, request, jsonify
2
+ import os, re, json
3
 
4
  app = Flask(__name__)
5
 
6
  # ────────────────────────── 1. CONFIGURATION ──────────────────────────
 
 
7
 
8
  # Domains that commonly block iframes
9
  BLOCKED_DOMAINS = [
 
12
  "ycombinator.com"
13
  ]
14
 
 
15
  # ────────────────────────── 2. CURATED CATEGORIES ──────────────────────────
16
  CATEGORIES = {
17
+ "Popular": [
 
18
 
19
+ "https://huggingface.co/spaces/fantaxy/ofai-flx-logo",
20
+ "https://huggingface.co/spaces/aiqtech/FLUX-Ghibli-Studio-LoRA",
21
+ "https://huggingface.co/spaces/seawolf2357/REALVISXL-V5",
22
+ "https://huggingface.co/spaces/fantos/flx8lora",
23
+ "https://huggingface.co/spaces/ginipick/Realtime-FLUX",
24
+ "https://huggingface.co/spaces/fantaxy/flx-pulid",
25
+ "https://huggingface.co/spaces/ginipick/FLUX-Prompt-Generator",
26
+ "https://huggingface.co/spaces/aiqtech/kofaceid",
27
+ "https://huggingface.co/spaces/aiqtech/flxgif",
28
+ "https://huggingface.co/spaces/fantos/flxfashmodel",
29
+ "https://huggingface.co/spaces/fantos/flxcontrol",
30
+ "https://huggingface.co/spaces/fantos/textcutobject",
31
+ "https://huggingface.co/spaces/seawolf2357/flxloraexp",
32
+ "https://huggingface.co/spaces/fantaxy/flxloraexp",
33
+ "https://huggingface.co/spaces/aiqtech/imaginpaint",
34
+ "https://huggingface.co/spaces/ginipick/FLUXllama",
35
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
36
+ "https://huggingface.co/spaces/fantaxy/flx-upscale",
37
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video",
38
+ "https://huggingface.co/spaces/fantos/VoiceClone",
39
+ "https://huggingface.co/spaces/fantaxy/Rolls-Royce",
40
+ "https://huggingface.co/spaces/aiqtech/FLUX-military",
41
+ "https://huggingface.co/spaces/fantaxy/FLUX-Animations",
42
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video2",
43
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
44
+ "https://huggingface.co/spaces/ginipick/Time-Stream",
45
+ "https://huggingface.co/spaces/seawolf2357/sd-prompt-gen",
46
+ "https://huggingface.co/spaces/openfree/MagicFace-V3",
47
+ "https://huggingface.co/spaces/Heartsync/adult",
48
+ "https://huggingface.co/spaces/Heartsync/wan2-1-fast-security",
49
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-REAL",
50
+ "https://huggingface.co/spaces/seawolf2357/img2vid",
51
+ "https://huggingface.co/spaces/openfree/image-to-vector",
52
+ "https://huggingface.co/spaces/openfree/DreamO-video",
53
+ "https://huggingface.co/spaces/VIDraft/FramePack_rotate_landscape",
54
+ "https://huggingface.co/spaces/fantaxy/Sound-AI-SFX",
55
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
56
+ "https://huggingface.co/spaces/Heartsync/FREE-NSFW-HUB",
57
+ "https://huggingface.co/spaces/Heartsync/NSFW-image",
58
+ "https://huggingface.co/spaces/Heartsync/NSFW-detection",
59
+ "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
60
+ "https://huggingface.co/spaces/ginigen/VEO3-Free",
61
+ "https://huggingface.co/spaces/ginigen/FLUX-Text-Tree-Image",
62
+ "https://huggingface.co/spaces/ginigen/text3d-r1",
63
+ "https://huggingface.co/spaces/VIDraft/stable-diffusion-3.5-large-turboX",
64
+
65
+ ],
66
+ "BEST": [
67
+
68
+ "https://huggingface.co/spaces/openfree/Cycle-Navigator",
69
+ "https://huggingface.co/spaces/ginigen/Flux-Kontext-FaceLORA",
70
+ "https://huggingface.co/spaces/ginigen/Seedance-Free",
71
+ "https://huggingface.co/spaces/VIDraft/SOMA-AGI",
72
+ "https://huggingface.co/spaces/aiqtech/Heatmap-Leaderboard",
73
+ "https://huggingface.co/spaces/VIDraft/DNA-CASINO",
74
+ "https://huggingface.co/spaces/aiqtech/SOMA-Oriental",
75
+ "https://huggingface.co/spaces/fantaxy/YTB-TEST",
76
+ "https://huggingface.co/spaces/aiqtech/Contributors-Leaderboard",
77
+ "https://huggingface.co/spaces/ginigen/text3d-r1",
78
+ "https://huggingface.co/spaces/VIDraft/stable-diffusion-3.5-large-turboX",
79
+ "https://huggingface.co/spaces/openfree/Korean-Leaderboard",
80
+ "https://huggingface.co/spaces/fantos/flxcontrol",
81
+ "https://huggingface.co/spaces/aiqtech/FLUX-Ghibli-Studio-LoRA",
82
+ "https://huggingface.co/spaces/openfree/AI-Podcast",
83
+ "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
84
+ "https://huggingface.co/spaces/ginigen/3D-LLAMA",
85
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
86
+ "https://huggingface.co/spaces/VIDraft/ACE-Singer",
87
+ "https://huggingface.co/spaces/ginipick/AI-BOOK",
88
+ "https://huggingface.co/spaces/immunobiotech/drug-discovery",
89
+ "https://huggingface.co/spaces/VIDraft/Robo-Beam",
90
+ "https://huggingface.co/spaces/fantaxy/fantasy-novel",
91
+ "https://huggingface.co/spaces/immunobiotech/Gemini-MICHELIN",
92
+ "https://huggingface.co/spaces/openfree/Chart-GPT",
93
+ "https://huggingface.co/spaces/ginipick/NH-Korea",
94
+ "https://huggingface.co/spaces/VIDraft/Voice-Clone-Podcast",
95
+ "https://huggingface.co/spaces/ginipick/Private-AI",
96
+ "https://huggingface.co/spaces/ginigen/Flux-VIDEO",
97
+ "https://huggingface.co/spaces/openfree/open-GAMMA",
98
+ "https://huggingface.co/spaces/ginipick/PharmAI-Korea",
99
+ "https://huggingface.co/spaces/ginipick/Pharmacy",
100
+ "https://huggingface.co/spaces/ginipick/PDF-EXAM",
101
+ "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
102
+ "https://huggingface.co/spaces/openfree/DreamO-video",
103
+ "https://huggingface.co/spaces/ginipick/10m-marketing",
104
+ "https://huggingface.co/spaces/VIDraft/voice-trans",
105
+ "https://huggingface.co/spaces/VIDraft/NH-Prediction",
106
+ "https://huggingface.co/spaces/fantos/flx8lora",
107
+ "https://huggingface.co/spaces/ginigen/MagicFace-V3",
108
+ "https://huggingface.co/spaces/openfree/Live-Podcast",
109
+ "https://huggingface.co/spaces/seawolf2357/ocrlatex",
110
+ "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
111
+ "https://huggingface.co/spaces/ginigen/VEO3-Free",
112
+ "https://huggingface.co/spaces/openfree/MagicFace-V3",
113
+ "https://huggingface.co/spaces/aiqtech/FLUX-military",
114
+ "https://huggingface.co/spaces/fantaxy/flxloraexp",
115
+ "https://huggingface.co/spaces/Heartsync/WAN2-1-fast-T2V-FusioniX",
116
+ "https://huggingface.co/spaces/ginigen/FLUXllama-Multilingual",
117
+ "https://huggingface.co/spaces/Heartsync/wan2-1-fast-security",
118
+ "https://huggingface.co/spaces/fantaxy/Rolls-Royce",
119
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-REAL",
120
+ "https://huggingface.co/spaces/ginipick/Realtime-FLUX",
121
+ "https://huggingface.co/spaces/aiqtech/imaginpaint",
122
+ "https://huggingface.co/spaces/aiqtech/flxgif",
123
+ "https://huggingface.co/spaces/fantos/flxfashmodel",
124
+ "https://huggingface.co/spaces/aiqtech/kofaceid",
125
+ "https://huggingface.co/spaces/ginipick/FLUX-Prompt-Generator",
126
+ "https://huggingface.co/spaces/seawolf2357/REALVISXL-V5",
127
+ "https://huggingface.co/spaces/fantaxy/FLUX-Animations",
128
+ "https://huggingface.co/spaces/fantaxy/flx-pulid",
129
+ "https://huggingface.co/spaces/fantaxy/ofai-flx-logo",
130
+ "https://huggingface.co/spaces/openfree/image-to-vector",
131
+ "https://huggingface.co/spaces/Heartsync/FREE-NSFW-HUB",
132
+ "https://huggingface.co/spaces/seawolf2357/sd-prompt-gen",
133
+ "https://huggingface.co/spaces/VIDraft/FramePack_rotate_landscape",
134
+ "https://huggingface.co/spaces/ginipick/FLUXllama",
135
+ "https://huggingface.co/spaces/Heartsync/NSFW-image",
136
+ "https://huggingface.co/spaces/seawolf2357/img2vid",
137
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video2",
138
+ "https://huggingface.co/spaces/Heartsync/NSFW-detection",
139
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video",
140
+ "https://huggingface.co/spaces/Heartsync/adult",
141
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
142
+ "https://huggingface.co/spaces/fantos/VoiceClone",
143
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
144
+ "https://huggingface.co/spaces/fantaxy/flx-upscale",
145
+ "https://huggingface.co/spaces/seawolf2357/flxloraexp",
146
+ "https://huggingface.co/spaces/ginipick/Time-Stream",
147
+ "https://huggingface.co/spaces/fantos/textcutobject",
148
+
149
+
150
+ ],
151
+ "NEW": [
152
+ "https://huggingface.co/spaces/VIDraft/ACE-Singer",
153
+ "https://huggingface.co/spaces/ginipick/AI-BOOK",
154
+ "https://huggingface.co/spaces/openfree/Best-AI",
155
  "https://huggingface.co/spaces/aiqtech/Heatmap-Leaderboard",
156
  "https://huggingface.co/spaces/VIDraft/DNA-CASINO",
157
+ "https://huggingface.co/spaces/ginigen/Seedance-Free",
158
+ "https://huggingface.co/spaces/aiqtech/SOMA-Oriental",
159
+ "https://huggingface.co/spaces/ginigen/Flux-Kontext-FaceLORA",
160
+ "https://huggingface.co/spaces/VIDraft/SOMA-AGI",
161
+ "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
162
+ "https://huggingface.co/spaces/openfree/Open-GAMMA",
163
+ "https://huggingface.co/spaces/ginigen/VEO3-Free",
164
+ "https://huggingface.co/spaces/Heartsync/WAN2-1-fast-T2V-FusioniX",
165
+ "https://huggingface.co/spaces/VIDraft/voice-trans",
166
+ "https://huggingface.co/spaces/VIDraft/Robo-Beam",
167
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-REAL",
168
+ "https://huggingface.co/spaces/fantaxy/fantasy-novel",
169
+ "https://huggingface.co/spaces/openfree/Chart-GPT",
170
+ "https://huggingface.co/spaces/Heartsync/Novel-NSFW",
171
+ "https://huggingface.co/spaces/ginigen/FLUX-Ghibli-LoRA2",
172
+ "https://huggingface.co/spaces/Heartsync/WAN-VIDEO-AUDIO",
173
+ "https://huggingface.co/spaces/Heartsync/wan2-1-fast-security",
174
+ "https://huggingface.co/spaces/ginigen/Flux-VIDEO",
175
+ "https://huggingface.co/spaces/aiqcamp/REMOVAL-TEXT-IMAGE",
176
+ "https://huggingface.co/spaces/VIDraft/Mistral-RAG-BitSix",
177
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video2",
178
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video",
179
+ "https://huggingface.co/spaces/fantaxy/YTB-TEST",
180
+ "https://huggingface.co/spaces/Heartsync/FREE-NSFW-HUB",
181
+ "https://huggingface.co/spaces/Heartsync/adult",
182
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
183
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
184
+ "https://huggingface.co/spaces/openfree/Live-Podcast",
185
+ "https://huggingface.co/spaces/openfree/AI-Podcast",
186
+ "https://huggingface.co/spaces/ginipick/NH-Korea",
187
+ "https://huggingface.co/spaces/VIDraft/NH-Prediction",
188
+ "https://huggingface.co/spaces/VIDraft/Voice-Clone-Podcast",
189
+ "https://huggingface.co/spaces/ginipick/PDF-EXAM",
190
+ "https://huggingface.co/spaces/openfree/Game-Gallery",
191
+ "https://huggingface.co/spaces/openfree/Vibe-Game",
192
+ "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
193
+ "https://huggingface.co/spaces/openfree/Cycle-Navigator",
194
+ "https://huggingface.co/spaces/openfree/DreamO-video",
195
+ "https://huggingface.co/spaces/Heartsync/NSFW-detection",
196
+
197
 
198
+ ],
199
+ "Productivity": [
200
+ "https://huggingface.co/spaces/aiqtech/Heatmap-Leaderboard",
201
+ "https://huggingface.co/spaces/VIDraft/DNA-CASINO",
202
  "https://huggingface.co/spaces/openfree/Open-GAMMA",
203
  "https://huggingface.co/spaces/VIDraft/Robo-Beam",
204
  "https://huggingface.co/spaces/VIDraft/voice-trans",
 
210
  "https://huggingface.co/spaces/ginigen/perflexity-clone",
211
  "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
212
  "https://huggingface.co/spaces/ginipick/10m-marketing",
 
213
  "https://huggingface.co/spaces/openfree/Live-Podcast",
214
  "https://huggingface.co/spaces/openfree/AI-Podcast",
215
  "https://huggingface.co/spaces/ginipick/QR-Canvas-plus",
 
222
  "https://huggingface.co/spaces/ginipick/Change-Hair",
223
  ],
224
  "Multimodal": [
 
225
  "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
 
226
  "https://huggingface.co/spaces/fantaxy/YTB-TEST",
227
  "https://huggingface.co/spaces/ginigen/Seedance-Free",
228
  "https://huggingface.co/spaces/Heartsync/VEO3-RealTime",
 
294
  "https://huggingface.co/spaces/openfree/VectorFlow",
295
  "https://huggingface.co/spaces/ginigen/3D-LLAMA",
296
  "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
 
297
  ],
298
  "LLM / VLM": [
299
  "https://huggingface.co/spaces/fantaxy/fantasy-novel",
 
314
  ],
315
  }
316
 
317
+ # ────────────────────────── 3. URL HELPERS ──────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  def direct_url(hf_url):
319
  m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
320
  if not m:
 
352
  # Default handling
353
  return url, "iframe"
354
 
355
+ # ────────────────────────── 4. API ROUTES ──────────────────────────
356
  @app.route('/api/category')
357
  def api_category():
358
  cat = request.args.get('name', '')
359
  urls = CATEGORIES.get(cat, [])
360
 
361
+ # Add pagination for categories
362
  page = int(request.args.get('page', 1))
363
+ per_page = int(request.args.get('per_page', 4))
364
 
365
  total_pages = max(1, (len(urls) + per_page - 1) // per_page)
366
  start = (page - 1) * per_page
 
384
  "total_pages": total_pages
385
  })
386
 
387
+ # ────────────────────────── 5. MAIN ROUTES ──────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  @app.route('/')
389
  def home():
390
  os.makedirs('templates', exist_ok=True)
 
402
  .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px;}
403
  .tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer;}
404
  .tab.active{background:#a78bfa;color:#1a202c;}
405
+ .tab.popular{background:#ff6b6b;color:white;}
406
+ .tab.popular.active{background:#fa5252;color:white;}
407
+ .tab.best{background:#4ecdc4;color:white;}
408
+ .tab.best.active{background:#38d9a9;color:white;}
409
+ .tab.new{background:#ffe066;color:#1a202c;}
410
+ .tab.new.active{background:#ffd43b;color:#1a202c;}
411
  /* Updated grid to show 2x2 layout */
412
  .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;padding:0 16px 60px;max-width:1200px;margin:0 auto;}
413
  @media(max-width:800px){.grid{grid-template-columns:1fr;}}
 
424
  .pagination{display:flex;justify-content:center;margin:20px 0;gap:10px;}
425
  .pagination button{padding:5px 15px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;}
426
  .pagination button:disabled{opacity:0.5;cursor:not-allowed;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  </style>
428
  </head>
429
  <body>
430
  <header style="text-align: center; padding: 20px; background: linear-gradient(135deg, #f6f8fb, #e2e8f0); border-bottom: 1px solid #ddd;">
431
+ <h1 style="margin-bottom: 10px;">🌟OPEN & Free: BEST AI Playground</h1>
432
  <p>
433
  <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>
434
+
435
+ <a href="https://huggingface.co/spaces/openfree/Best-AI" target="_blank"><img src="https://img.shields.io/static/v1?label=OpenFree&message=BEST%20AI%20Services&color=%230000ff&labelColor=%23000080&logo=huggingface&logoColor=%23ffa500&style=for-the-badge" alt="badge"></a>
436
+ </p>
437
  </header>
438
  <div class="tabs" id="tabs"></div>
439
  <div id="content"></div>
 
447
  let currentPage = 1;
448
 
449
  // Simple utility functions
 
 
 
 
 
 
 
 
 
 
 
450
  function makeRequest(url, method, data, callback) {
451
  const xhr = new XMLHttpRequest();
452
  xhr.open(method, url, true);
 
513
  });
514
  }
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  // Create tabs
517
+ // Special tabs first (Popular, BEST, NEW)
518
+ ['Popular', 'BEST', 'NEW'].forEach(specialCat => {
 
 
 
 
 
 
 
 
519
  const b = document.createElement('button');
520
+ b.className = 'tab ' + specialCat.toLowerCase();
521
+ b.textContent = specialCat;
522
+ b.dataset.c = specialCat;
523
+ b.onclick = function() { loadCategory(specialCat, 1); };
524
  tabs.appendChild(b);
525
  });
526
 
527
+ // Regular category tabs
528
+ cats.forEach(c => {
529
+ if (!['Popular', 'BEST', 'NEW'].includes(c)) {
530
+ const b = document.createElement('button');
531
+ b.className = 'tab';
532
+ b.textContent = c;
533
+ b.dataset.c = c;
534
+ b.onclick = function() { loadCategory(c, 1); };
535
+ tabs.appendChild(b);
536
+ }
537
+ });
538
 
539
+ // Start with Popular tab
540
+ loadCategory('Popular', 1);
541
  </script>
542
  </body>
543
  </html>''')
 
545
  # Return the rendered template
546
  return render_template('index.html', cats=list(CATEGORIES.keys()))
547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  if __name__ == '__main__':
 
 
 
 
 
 
 
 
 
 
 
 
549
  app.run(host='0.0.0.0', port=7860)