Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
from flask import Flask, render_template, request, jsonify
|
2 |
-
import os, re, json
|
3 |
|
4 |
app = Flask(__name__)
|
5 |
|
6 |
# ββββββββββββββββββββββββββ 1. CONFIGURATION ββββββββββββββββββββββββββ
|
7 |
-
DB_FILE = "favorite_sites.json" #
|
|
|
8 |
|
9 |
# Domains that commonly block iframes
|
10 |
BLOCKED_DOMAINS = [
|
@@ -15,7 +16,7 @@ BLOCKED_DOMAINS = [
|
|
15 |
|
16 |
# ββββββββββββββββββββββββββ 2. CURATED CATEGORIES ββββββββββββββββββββββββββ
|
17 |
CATEGORIES = {
|
18 |
-
"
|
19 |
"https://huggingface.co/spaces/ginigen/perflexity-clone",
|
20 |
"https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
|
21 |
"https://huggingface.co/spaces/VIDraft/mouse-webgen",
|
@@ -26,7 +27,7 @@ CATEGORIES = {
|
|
26 |
"https://huggingface.co/spaces/fantaxy/Space-Leaderboard",
|
27 |
"https://huggingface.co/spaces/openfree/Korean-Leaderboard",
|
28 |
],
|
29 |
-
"
|
30 |
"https://huggingface.co/spaces/openfree/DreamO-video",
|
31 |
"https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
|
32 |
"https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
|
@@ -36,7 +37,7 @@ CATEGORIES = {
|
|
36 |
"https://huggingface.co/spaces/aiqcamp/MCP-kokoro",
|
37 |
"https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
|
38 |
],
|
39 |
-
"
|
40 |
"https://huggingface.co/spaces/ginigen/blogger",
|
41 |
"https://huggingface.co/spaces/VIDraft/money-radar",
|
42 |
"https://huggingface.co/spaces/immunobiotech/drug-discovery",
|
@@ -46,7 +47,7 @@ CATEGORIES = {
|
|
46 |
"https://huggingface.co/spaces/ginipick/AgentX-Papers",
|
47 |
"https://huggingface.co/spaces/openfree/Cycle-Navigator",
|
48 |
],
|
49 |
-
"
|
50 |
"https://huggingface.co/spaces/ginigen/interior-design",
|
51 |
"https://huggingface.co/spaces/ginigen/Workflow-Canvas",
|
52 |
"https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
|
@@ -60,7 +61,7 @@ CATEGORIES = {
|
|
60 |
"https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
|
61 |
"https://huggingface.co/spaces/ginigen/3D-LLAMA",
|
62 |
],
|
63 |
-
"
|
64 |
"https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
|
65 |
"https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
|
66 |
"https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
|
@@ -73,11 +74,36 @@ CATEGORIES = {
|
|
73 |
|
74 |
# ββββββββββββββββββββββββββ 3. DATABASE FUNCTIONS ββββββββββββββββββββββββββ
|
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 |
-
def
|
|
|
81 |
try:
|
82 |
with open(DB_FILE, "r", encoding="utf-8") as f:
|
83 |
raw = json.load(f)
|
@@ -85,7 +111,8 @@ def load_db():
|
|
85 |
except Exception:
|
86 |
return []
|
87 |
|
88 |
-
def
|
|
|
89 |
try:
|
90 |
with open(DB_FILE, "w", encoding="utf-8") as f:
|
91 |
json.dump(lst, f, ensure_ascii=False, indent=2)
|
@@ -93,6 +120,87 @@ def save_db(lst):
|
|
93 |
except Exception:
|
94 |
return False
|
95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
# ββββββββββββββββββββββββββ 4. URL HELPERS ββββββββββββββββββββββββββ
|
97 |
def direct_url(hf_url):
|
98 |
m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
|
@@ -148,7 +256,9 @@ def api_category():
|
|
148 |
|
149 |
@app.route('/api/favorites')
|
150 |
def api_favorites():
|
|
|
151 |
urls = load_db()
|
|
|
152 |
page = int(request.args.get('page', 1))
|
153 |
per_page = int(request.args.get('per_page', 9))
|
154 |
|
@@ -189,12 +299,16 @@ def add_url():
|
|
189 |
if not url:
|
190 |
return jsonify({"success": False, "message": "URL is required"})
|
191 |
|
192 |
-
|
193 |
-
if url
|
194 |
return jsonify({"success": False, "message": "URL already exists"})
|
195 |
|
196 |
-
|
197 |
-
|
|
|
|
|
|
|
|
|
198 |
return jsonify({"success": True, "message": "URL added successfully"})
|
199 |
|
200 |
@app.route('/api/url/update', methods=['POST'])
|
@@ -205,25 +319,40 @@ def update_url():
|
|
205 |
if not new:
|
206 |
return jsonify({"success": False, "message": "New URL is required"})
|
207 |
|
208 |
-
|
|
|
|
|
|
|
|
|
|
|
209 |
try:
|
210 |
idx = data.index(old)
|
211 |
data[idx] = new
|
212 |
-
|
213 |
-
return jsonify({"success": True, "message": "URL updated successfully"})
|
214 |
except ValueError:
|
215 |
-
|
|
|
|
|
|
|
|
|
216 |
|
217 |
@app.route('/api/url/delete', methods=['POST'])
|
218 |
def delete_url():
|
219 |
url = request.form.get('url', '')
|
220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
try:
|
222 |
data.remove(url)
|
223 |
-
|
224 |
-
return jsonify({"success": True, "message": "URL deleted successfully"})
|
225 |
except ValueError:
|
226 |
-
|
|
|
|
|
227 |
|
228 |
# ββββββββββββββββββββββββββ 6. MAIN ROUTES ββββββββββββββββββββββββββ
|
229 |
@app.route('/')
|
@@ -279,6 +408,7 @@ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
|
|
279 |
<body>
|
280 |
<div class="tabs" id="tabs"></div>
|
281 |
<div id="content"></div>
|
|
|
282 |
<script>
|
283 |
// Basic configuration
|
284 |
const cats = {{cats|tojson}};
|
@@ -286,6 +416,7 @@ const tabs = document.getElementById('tabs');
|
|
286 |
const content = document.getElementById('content');
|
287 |
let active = "";
|
288 |
let currentPage = 1;
|
|
|
289 |
// Simple utility functions
|
290 |
function loadHTML(url, callback) {
|
291 |
const xhr = new XMLHttpRequest();
|
@@ -297,6 +428,7 @@ function loadHTML(url, callback) {
|
|
297 |
};
|
298 |
xhr.send();
|
299 |
}
|
|
|
300 |
function makeRequest(url, method, data, callback) {
|
301 |
const xhr = new XMLHttpRequest();
|
302 |
xhr.open(method, url, true);
|
@@ -311,11 +443,13 @@ function makeRequest(url, method, data, callback) {
|
|
311 |
xhr.send();
|
312 |
}
|
313 |
}
|
|
|
314 |
function updateTabs() {
|
315 |
Array.from(tabs.children).forEach(b => {
|
316 |
b.classList.toggle('active', b.dataset.c === active);
|
317 |
});
|
318 |
}
|
|
|
319 |
// Tab handlers
|
320 |
function loadCategory(cat) {
|
321 |
if(cat === active) return;
|
@@ -330,6 +464,7 @@ function loadCategory(cat) {
|
|
330 |
data.forEach(item => {
|
331 |
html += `
|
332 |
<div class="card">
|
|
|
333 |
<div class="frame">
|
334 |
<iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
|
335 |
</div>
|
@@ -344,6 +479,7 @@ function loadCategory(cat) {
|
|
344 |
content.innerHTML = html;
|
345 |
});
|
346 |
}
|
|
|
347 |
function loadFavorites(page) {
|
348 |
if(active === 'Favorites' && currentPage === page) return;
|
349 |
active = 'Favorites';
|
@@ -401,6 +537,7 @@ function loadFavorites(page) {
|
|
401 |
content.innerHTML = html;
|
402 |
});
|
403 |
}
|
|
|
404 |
function loadManage() {
|
405 |
if(active === 'Manage') return;
|
406 |
active = 'Manage';
|
@@ -423,6 +560,7 @@ function loadManage() {
|
|
423 |
|
424 |
loadUrlList();
|
425 |
}
|
|
|
426 |
// URL management functions
|
427 |
function loadUrlList() {
|
428 |
makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
|
@@ -435,12 +573,15 @@ function loadUrlList() {
|
|
435 |
|
436 |
let html = '';
|
437 |
data.items.forEach(item => {
|
|
|
|
|
|
|
438 |
html += `
|
439 |
<div class="url-item">
|
440 |
<div>${item.url}</div>
|
441 |
<div class="url-controls">
|
442 |
-
<button class="btn" onclick="editUrl('${
|
443 |
-
<button class="btn btn-danger" onclick="deleteUrl('${
|
444 |
</div>
|
445 |
</div>
|
446 |
`;
|
@@ -449,6 +590,7 @@ function loadUrlList() {
|
|
449 |
urlList.innerHTML = html;
|
450 |
});
|
451 |
}
|
|
|
452 |
function addUrl() {
|
453 |
const url = document.getElementById('new-url').value.trim();
|
454 |
|
@@ -465,40 +607,59 @@ function addUrl() {
|
|
465 |
if(data.success) {
|
466 |
document.getElementById('new-url').value = '';
|
467 |
loadUrlList();
|
|
|
|
|
|
|
|
|
468 |
}
|
469 |
});
|
470 |
}
|
|
|
471 |
function editUrl(url) {
|
472 |
-
|
|
|
|
|
473 |
|
474 |
-
if(!newUrl || newUrl ===
|
475 |
|
476 |
const formData = new FormData();
|
477 |
-
formData.append('old',
|
478 |
formData.append('new', newUrl);
|
479 |
|
480 |
makeRequest('/api/url/update', 'POST', formData, function(data) {
|
481 |
if(data.success) {
|
482 |
loadUrlList();
|
|
|
|
|
|
|
|
|
483 |
} else {
|
484 |
alert(data.message);
|
485 |
}
|
486 |
});
|
487 |
}
|
|
|
488 |
function deleteUrl(url) {
|
|
|
|
|
489 |
if(!confirm('Are you sure you want to delete this URL?')) return;
|
490 |
|
491 |
const formData = new FormData();
|
492 |
-
formData.append('url',
|
493 |
|
494 |
makeRequest('/api/url/delete', 'POST', formData, function(data) {
|
495 |
if(data.success) {
|
496 |
loadUrlList();
|
|
|
|
|
|
|
|
|
497 |
} else {
|
498 |
alert(data.message);
|
499 |
}
|
500 |
});
|
501 |
}
|
|
|
502 |
function showStatus(id, message, success) {
|
503 |
const status = document.getElementById(id);
|
504 |
status.textContent = message;
|
@@ -507,6 +668,7 @@ function showStatus(id, message, success) {
|
|
507 |
status.className = 'status';
|
508 |
}, 3000);
|
509 |
}
|
|
|
510 |
// Create tabs
|
511 |
// Favorites tab first
|
512 |
const favTab = document.createElement('button');
|
@@ -515,6 +677,7 @@ favTab.textContent = 'Favorites';
|
|
515 |
favTab.dataset.c = 'Favorites';
|
516 |
favTab.onclick = function() { loadFavorites(1); };
|
517 |
tabs.appendChild(favTab);
|
|
|
518 |
// Category tabs
|
519 |
cats.forEach(c => {
|
520 |
const b = document.createElement('button');
|
@@ -524,6 +687,7 @@ cats.forEach(c => {
|
|
524 |
b.onclick = function() { loadCategory(c); };
|
525 |
tabs.appendChild(b);
|
526 |
});
|
|
|
527 |
// Manage tab last
|
528 |
const manageTab = document.createElement('button');
|
529 |
manageTab.className = 'tab manage';
|
@@ -531,6 +695,7 @@ manageTab.textContent = 'Manage';
|
|
531 |
manageTab.dataset.c = 'Manage';
|
532 |
manageTab.onclick = function() { loadManage(); };
|
533 |
tabs.appendChild(manageTab);
|
|
|
534 |
// Start with Favorites tab
|
535 |
loadFavorites(1);
|
536 |
</script>
|
@@ -543,5 +708,12 @@ loadFavorites(1);
|
|
543 |
# Initialize database on startup
|
544 |
init_db()
|
545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
546 |
if __name__ == '__main__':
|
547 |
app.run(host='0.0.0.0', port=7860)
|
|
|
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 = [
|
|
|
16 |
|
17 |
# ββββββββββββββββββββββββββ 2. CURATED CATEGORIES ββββββββββββββββββββββββββ
|
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",
|
|
|
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",
|
|
|
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",
|
|
|
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",
|
|
|
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",
|
|
|
74 |
|
75 |
# ββββββββββββββββββββββββββ 3. DATABASE FUNCTIONS ββββββββββββββββββββββββββ
|
76 |
def init_db():
|
77 |
+
# Initialize JSON file if it doesn't exist
|
78 |
if not os.path.exists(DB_FILE):
|
79 |
with open(DB_FILE, "w", encoding="utf-8") as f:
|
80 |
json.dump([], f, ensure_ascii=False)
|
81 |
+
|
82 |
+
# Initialize SQLite database
|
83 |
+
conn = sqlite3.connect(SQLITE_DB)
|
84 |
+
cursor = conn.cursor()
|
85 |
+
cursor.execute('''
|
86 |
+
CREATE TABLE IF NOT EXISTS urls (
|
87 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
88 |
+
url TEXT UNIQUE NOT NULL,
|
89 |
+
date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
90 |
+
)
|
91 |
+
''')
|
92 |
+
conn.commit()
|
93 |
+
|
94 |
+
# If we have data in JSON but not in SQLite (first run with new SQLite DB),
|
95 |
+
# migrate the data from JSON to SQLite
|
96 |
+
json_urls = load_json()
|
97 |
+
if json_urls:
|
98 |
+
db_urls = load_db_sqlite()
|
99 |
+
for url in json_urls:
|
100 |
+
if url not in db_urls:
|
101 |
+
add_url_to_sqlite(url)
|
102 |
+
|
103 |
+
conn.close()
|
104 |
|
105 |
+
def load_json():
|
106 |
+
"""Load URLs from JSON file (for backward compatibility)"""
|
107 |
try:
|
108 |
with open(DB_FILE, "r", encoding="utf-8") as f:
|
109 |
raw = json.load(f)
|
|
|
111 |
except Exception:
|
112 |
return []
|
113 |
|
114 |
+
def save_json(lst):
|
115 |
+
"""Save URLs to JSON file (for backward compatibility)"""
|
116 |
try:
|
117 |
with open(DB_FILE, "w", encoding="utf-8") as f:
|
118 |
json.dump(lst, f, ensure_ascii=False, indent=2)
|
|
|
120 |
except Exception:
|
121 |
return False
|
122 |
|
123 |
+
def load_db_sqlite():
|
124 |
+
"""Load URLs from SQLite database"""
|
125 |
+
conn = sqlite3.connect(SQLITE_DB)
|
126 |
+
cursor = conn.cursor()
|
127 |
+
cursor.execute("SELECT url FROM urls ORDER BY date_added DESC")
|
128 |
+
urls = [row[0] for row in cursor.fetchall()]
|
129 |
+
conn.close()
|
130 |
+
return urls
|
131 |
+
|
132 |
+
def add_url_to_sqlite(url):
|
133 |
+
"""Add a URL to SQLite database"""
|
134 |
+
conn = sqlite3.connect(SQLITE_DB)
|
135 |
+
cursor = conn.cursor()
|
136 |
+
try:
|
137 |
+
cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
|
138 |
+
conn.commit()
|
139 |
+
success = True
|
140 |
+
except sqlite3.IntegrityError:
|
141 |
+
# URL already exists
|
142 |
+
success = False
|
143 |
+
conn.close()
|
144 |
+
return success
|
145 |
+
|
146 |
+
def update_url_in_sqlite(old_url, new_url):
|
147 |
+
"""Update a URL in SQLite database"""
|
148 |
+
conn = sqlite3.connect(SQLITE_DB)
|
149 |
+
cursor = conn.cursor()
|
150 |
+
try:
|
151 |
+
cursor.execute("UPDATE urls SET url = ? WHERE url = ?", (new_url, old_url))
|
152 |
+
if cursor.rowcount > 0:
|
153 |
+
conn.commit()
|
154 |
+
success = True
|
155 |
+
else:
|
156 |
+
success = False
|
157 |
+
except sqlite3.IntegrityError:
|
158 |
+
# New URL already exists
|
159 |
+
success = False
|
160 |
+
conn.close()
|
161 |
+
return success
|
162 |
+
|
163 |
+
def delete_url_from_sqlite(url):
|
164 |
+
"""Delete a URL from SQLite database"""
|
165 |
+
conn = sqlite3.connect(SQLITE_DB)
|
166 |
+
cursor = conn.cursor()
|
167 |
+
cursor.execute("DELETE FROM urls WHERE url = ?", (url,))
|
168 |
+
if cursor.rowcount > 0:
|
169 |
+
conn.commit()
|
170 |
+
success = True
|
171 |
+
else:
|
172 |
+
success = False
|
173 |
+
conn.close()
|
174 |
+
return success
|
175 |
+
|
176 |
+
def load_db():
|
177 |
+
"""Primary function to load URLs - prioritizes SQLite DB but falls back to JSON"""
|
178 |
+
urls = load_db_sqlite()
|
179 |
+
if not urls:
|
180 |
+
# If SQLite DB is empty, try loading from JSON
|
181 |
+
urls = load_json()
|
182 |
+
# If we found URLs in JSON, migrate them to SQLite
|
183 |
+
for url in urls:
|
184 |
+
add_url_to_sqlite(url)
|
185 |
+
return urls
|
186 |
+
|
187 |
+
def save_db(lst):
|
188 |
+
"""Save URLs to both SQLite and JSON"""
|
189 |
+
# Get existing URLs from SQLite for comparison
|
190 |
+
existing_urls = load_db_sqlite()
|
191 |
+
|
192 |
+
# Clear all URLs from SQLite and add the new list
|
193 |
+
conn = sqlite3.connect(SQLITE_DB)
|
194 |
+
cursor = conn.cursor()
|
195 |
+
cursor.execute("DELETE FROM urls")
|
196 |
+
for url in lst:
|
197 |
+
cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
|
198 |
+
conn.commit()
|
199 |
+
conn.close()
|
200 |
+
|
201 |
+
# Also save to JSON for backward compatibility
|
202 |
+
return save_json(lst)
|
203 |
+
|
204 |
# ββββββββββββββββββββββββββ 4. URL HELPERS ββββββββββββββββββββββββββ
|
205 |
def direct_url(hf_url):
|
206 |
m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
|
|
|
256 |
|
257 |
@app.route('/api/favorites')
|
258 |
def api_favorites():
|
259 |
+
# Load URLs from SQLite database
|
260 |
urls = load_db()
|
261 |
+
|
262 |
page = int(request.args.get('page', 1))
|
263 |
per_page = int(request.args.get('per_page', 9))
|
264 |
|
|
|
299 |
if not url:
|
300 |
return jsonify({"success": False, "message": "URL is required"})
|
301 |
|
302 |
+
# Check if URL already exists in database
|
303 |
+
if not add_url_to_sqlite(url):
|
304 |
return jsonify({"success": False, "message": "URL already exists"})
|
305 |
|
306 |
+
# Also update JSON file for backward compatibility
|
307 |
+
data = load_json()
|
308 |
+
if url not in data:
|
309 |
+
data.insert(0, url)
|
310 |
+
save_json(data)
|
311 |
+
|
312 |
return jsonify({"success": True, "message": "URL added successfully"})
|
313 |
|
314 |
@app.route('/api/url/update', methods=['POST'])
|
|
|
319 |
if not new:
|
320 |
return jsonify({"success": False, "message": "New URL is required"})
|
321 |
|
322 |
+
# Update in SQLite DB
|
323 |
+
if not update_url_in_sqlite(old, new):
|
324 |
+
return jsonify({"success": False, "message": "URL not found or new URL already exists"})
|
325 |
+
|
326 |
+
# Also update JSON file for backward compatibility
|
327 |
+
data = load_json()
|
328 |
try:
|
329 |
idx = data.index(old)
|
330 |
data[idx] = new
|
331 |
+
save_json(data)
|
|
|
332 |
except ValueError:
|
333 |
+
# If URL not in JSON, add it
|
334 |
+
data.append(new)
|
335 |
+
save_json(data)
|
336 |
+
|
337 |
+
return jsonify({"success": True, "message": "URL updated successfully"})
|
338 |
|
339 |
@app.route('/api/url/delete', methods=['POST'])
|
340 |
def delete_url():
|
341 |
url = request.form.get('url', '')
|
342 |
+
|
343 |
+
# Delete from SQLite DB
|
344 |
+
if not delete_url_from_sqlite(url):
|
345 |
+
return jsonify({"success": False, "message": "URL not found"})
|
346 |
+
|
347 |
+
# Also update JSON file for backward compatibility
|
348 |
+
data = load_json()
|
349 |
try:
|
350 |
data.remove(url)
|
351 |
+
save_json(data)
|
|
|
352 |
except ValueError:
|
353 |
+
pass
|
354 |
+
|
355 |
+
return jsonify({"success": True, "message": "URL deleted successfully"})
|
356 |
|
357 |
# ββββββββββββββββββββββββββ 6. MAIN ROUTES ββββββββββββββββββββββββββ
|
358 |
@app.route('/')
|
|
|
408 |
<body>
|
409 |
<div class="tabs" id="tabs"></div>
|
410 |
<div id="content"></div>
|
411 |
+
|
412 |
<script>
|
413 |
// Basic configuration
|
414 |
const cats = {{cats|tojson}};
|
|
|
416 |
const content = document.getElementById('content');
|
417 |
let active = "";
|
418 |
let currentPage = 1;
|
419 |
+
|
420 |
// Simple utility functions
|
421 |
function loadHTML(url, callback) {
|
422 |
const xhr = new XMLHttpRequest();
|
|
|
428 |
};
|
429 |
xhr.send();
|
430 |
}
|
431 |
+
|
432 |
function makeRequest(url, method, data, callback) {
|
433 |
const xhr = new XMLHttpRequest();
|
434 |
xhr.open(method, url, true);
|
|
|
443 |
xhr.send();
|
444 |
}
|
445 |
}
|
446 |
+
|
447 |
function updateTabs() {
|
448 |
Array.from(tabs.children).forEach(b => {
|
449 |
b.classList.toggle('active', b.dataset.c === active);
|
450 |
});
|
451 |
}
|
452 |
+
|
453 |
// Tab handlers
|
454 |
function loadCategory(cat) {
|
455 |
if(cat === active) return;
|
|
|
464 |
data.forEach(item => {
|
465 |
html += `
|
466 |
<div class="card">
|
467 |
+
<div class="card-label label-live">LIVE</div>
|
468 |
<div class="frame">
|
469 |
<iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
|
470 |
</div>
|
|
|
479 |
content.innerHTML = html;
|
480 |
});
|
481 |
}
|
482 |
+
|
483 |
function loadFavorites(page) {
|
484 |
if(active === 'Favorites' && currentPage === page) return;
|
485 |
active = 'Favorites';
|
|
|
537 |
content.innerHTML = html;
|
538 |
});
|
539 |
}
|
540 |
+
|
541 |
function loadManage() {
|
542 |
if(active === 'Manage') return;
|
543 |
active = 'Manage';
|
|
|
560 |
|
561 |
loadUrlList();
|
562 |
}
|
563 |
+
|
564 |
// URL management functions
|
565 |
function loadUrlList() {
|
566 |
makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
|
|
|
573 |
|
574 |
let html = '';
|
575 |
data.items.forEach(item => {
|
576 |
+
// Escape the URL to prevent JavaScript injection when used in onclick handlers
|
577 |
+
const escapedUrl = item.url.replace(/'/g, "\\'");
|
578 |
+
|
579 |
html += `
|
580 |
<div class="url-item">
|
581 |
<div>${item.url}</div>
|
582 |
<div class="url-controls">
|
583 |
+
<button class="btn" onclick="editUrl('${escapedUrl}')">Edit</button>
|
584 |
+
<button class="btn btn-danger" onclick="deleteUrl('${escapedUrl}')">Delete</button>
|
585 |
</div>
|
586 |
</div>
|
587 |
`;
|
|
|
590 |
urlList.innerHTML = html;
|
591 |
});
|
592 |
}
|
593 |
+
|
594 |
function addUrl() {
|
595 |
const url = document.getElementById('new-url').value.trim();
|
596 |
|
|
|
607 |
if(data.success) {
|
608 |
document.getElementById('new-url').value = '';
|
609 |
loadUrlList();
|
610 |
+
// If currently in Favorites tab, reload to see changes immediately
|
611 |
+
if(active === 'Favorites') {
|
612 |
+
loadFavorites(currentPage);
|
613 |
+
}
|
614 |
}
|
615 |
});
|
616 |
}
|
617 |
+
|
618 |
function editUrl(url) {
|
619 |
+
// Decode URL if it was previously escaped
|
620 |
+
const decodedUrl = url.replace(/\\'/g, "'");
|
621 |
+
const newUrl = prompt('Edit URL:', decodedUrl);
|
622 |
|
623 |
+
if(!newUrl || newUrl === decodedUrl) return;
|
624 |
|
625 |
const formData = new FormData();
|
626 |
+
formData.append('old', decodedUrl);
|
627 |
formData.append('new', newUrl);
|
628 |
|
629 |
makeRequest('/api/url/update', 'POST', formData, function(data) {
|
630 |
if(data.success) {
|
631 |
loadUrlList();
|
632 |
+
// If currently in Favorites tab, reload to see changes immediately
|
633 |
+
if(active === 'Favorites') {
|
634 |
+
loadFavorites(currentPage);
|
635 |
+
}
|
636 |
} else {
|
637 |
alert(data.message);
|
638 |
}
|
639 |
});
|
640 |
}
|
641 |
+
|
642 |
function deleteUrl(url) {
|
643 |
+
// Decode URL if it was previously escaped
|
644 |
+
const decodedUrl = url.replace(/\\'/g, "'");
|
645 |
if(!confirm('Are you sure you want to delete this URL?')) return;
|
646 |
|
647 |
const formData = new FormData();
|
648 |
+
formData.append('url', decodedUrl);
|
649 |
|
650 |
makeRequest('/api/url/delete', 'POST', formData, function(data) {
|
651 |
if(data.success) {
|
652 |
loadUrlList();
|
653 |
+
// If currently in Favorites tab, reload to see changes immediately
|
654 |
+
if(active === 'Favorites') {
|
655 |
+
loadFavorites(currentPage);
|
656 |
+
}
|
657 |
} else {
|
658 |
alert(data.message);
|
659 |
}
|
660 |
});
|
661 |
}
|
662 |
+
|
663 |
function showStatus(id, message, success) {
|
664 |
const status = document.getElementById(id);
|
665 |
status.textContent = message;
|
|
|
668 |
status.className = 'status';
|
669 |
}, 3000);
|
670 |
}
|
671 |
+
|
672 |
// Create tabs
|
673 |
// Favorites tab first
|
674 |
const favTab = document.createElement('button');
|
|
|
677 |
favTab.dataset.c = 'Favorites';
|
678 |
favTab.onclick = function() { loadFavorites(1); };
|
679 |
tabs.appendChild(favTab);
|
680 |
+
|
681 |
// Category tabs
|
682 |
cats.forEach(c => {
|
683 |
const b = document.createElement('button');
|
|
|
687 |
b.onclick = function() { loadCategory(c); };
|
688 |
tabs.appendChild(b);
|
689 |
});
|
690 |
+
|
691 |
// Manage tab last
|
692 |
const manageTab = document.createElement('button');
|
693 |
manageTab.className = 'tab manage';
|
|
|
695 |
manageTab.dataset.c = 'Manage';
|
696 |
manageTab.onclick = function() { loadManage(); };
|
697 |
tabs.appendChild(manageTab);
|
698 |
+
|
699 |
// Start with Favorites tab
|
700 |
loadFavorites(1);
|
701 |
</script>
|
|
|
708 |
# Initialize database on startup
|
709 |
init_db()
|
710 |
|
711 |
+
# Create a backup of the database at regular intervals
|
712 |
+
@app.before_first_request
|
713 |
+
def setup_db_backup():
|
714 |
+
# Make sure we have the latest data in both JSON and SQLite
|
715 |
+
urls = load_db()
|
716 |
+
save_json(urls)
|
717 |
+
|
718 |
if __name__ == '__main__':
|
719 |
app.run(host='0.0.0.0', port=7860)
|