soxogvv commited on
Commit
80cdadf
Β·
verified Β·
1 Parent(s): 0d22716

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +732 -0
  2. requirements.txt +1 -0
app.py ADDED
@@ -0,0 +1,732 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import uuid
4
+ import json
5
+ import time
6
+ import threading
7
+ import subprocess
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse, unquote
10
+ from flask import Flask, request, jsonify, send_from_directory, render_template_string
11
+
12
+ app = Flask(__name__)
13
+
14
+ DOWNLOAD_DIR = Path("downloads")
15
+ DOWNLOAD_DIR.mkdir(exist_ok=True)
16
+ HISTORY_FILE = Path("history.json")
17
+
18
+ # In-memory progress tracker
19
+ progress_store = {}
20
+ history_lock = threading.Lock()
21
+
22
+
23
+ def load_history():
24
+ if HISTORY_FILE.exists():
25
+ with open(HISTORY_FILE, "r") as f:
26
+ return json.load(f)
27
+ return []
28
+
29
+
30
+ def save_history(entry):
31
+ with history_lock:
32
+ history = load_history()
33
+ history.insert(0, entry)
34
+ with open(HISTORY_FILE, "w") as f:
35
+ json.dump(history, f, indent=2)
36
+
37
+
38
+ def get_filename_from_url(url):
39
+ parsed = urlparse(url)
40
+ name = unquote(parsed.path.split("/")[-1])
41
+ if not name or "." not in name:
42
+ name = f"file_{uuid.uuid4().hex[:8]}"
43
+ return name
44
+
45
+
46
+ def download_with_wget(task_id, url, save_path):
47
+ """Download using wget with progress parsing."""
48
+ progress_store[task_id] = {
49
+ "status": "downloading",
50
+ "progress": 0,
51
+ "speed": "",
52
+ "eta": "",
53
+ "size": "",
54
+ "error": None,
55
+ "filename": save_path.name,
56
+ }
57
+
58
+ cmd = [
59
+ "wget",
60
+ "--no-check-certificate",
61
+ "--retry-connrefused",
62
+ "--tries=5",
63
+ "--timeout=30",
64
+ "--waitretry=3",
65
+ "--limit-rate=0", # No rate limit = max speed
66
+ "--progress=dot:mega",
67
+ "-O", str(save_path),
68
+ url,
69
+ ]
70
+
71
+ try:
72
+ process = subprocess.Popen(
73
+ cmd,
74
+ stderr=subprocess.PIPE,
75
+ stdout=subprocess.PIPE,
76
+ text=True,
77
+ bufsize=1,
78
+ )
79
+
80
+ dots_per_mb = 0
81
+ total_dots = 0
82
+
83
+ for line in process.stderr:
84
+ line = line.strip()
85
+ if not line:
86
+ continue
87
+
88
+ # Parse wget mega progress: dots represent chunks
89
+ if "." in line or "K" in line or "M" in line or "%" in line:
90
+ # Try to parse percentage from lines like "100% 2.50M 10.0MB/s 0s"
91
+ pct_match = re.search(r'(\d+)%', line)
92
+ speed_match = re.search(r'([\d.]+\s*[KMG]B/s)', line)
93
+ eta_match = re.search(r'(\d+[smhd]+)\s*$', line)
94
+ size_match = re.search(r'([\d.]+\s*[KMGkmg])', line)
95
+
96
+ if pct_match:
97
+ progress_store[task_id]["progress"] = int(pct_match.group(1))
98
+ if speed_match:
99
+ progress_store[task_id]["speed"] = speed_match.group(1)
100
+ if eta_match:
101
+ progress_store[task_id]["eta"] = eta_match.group(1)
102
+
103
+ # Count dots for rough estimation when no percentage shown
104
+ dot_count = line.count(".")
105
+ total_dots += dot_count
106
+
107
+ process.wait()
108
+
109
+ if process.returncode == 0 and save_path.exists():
110
+ file_size = save_path.stat().st_size
111
+ size_str = format_size(file_size)
112
+ progress_store[task_id].update({
113
+ "status": "done",
114
+ "progress": 100,
115
+ "size": size_str,
116
+ "speed": "",
117
+ "eta": "",
118
+ })
119
+ save_history({
120
+ "task_id": task_id,
121
+ "url": url,
122
+ "filename": save_path.name,
123
+ "size": size_str,
124
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
125
+ "path": str(save_path),
126
+ })
127
+ else:
128
+ stderr_output = ""
129
+ try:
130
+ stderr_output = process.stderr.read()
131
+ except Exception:
132
+ pass
133
+ progress_store[task_id].update({
134
+ "status": "error",
135
+ "error": f"wget exited with code {process.returncode}. {stderr_output[:200]}",
136
+ })
137
+ if save_path.exists():
138
+ save_path.unlink()
139
+
140
+ except FileNotFoundError:
141
+ progress_store[task_id].update({
142
+ "status": "error",
143
+ "error": "wget is not installed. Install it with: sudo apt install wget",
144
+ })
145
+ except Exception as e:
146
+ progress_store[task_id].update({
147
+ "status": "error",
148
+ "error": str(e),
149
+ })
150
+
151
+
152
+ def format_size(size_bytes):
153
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
154
+ if size_bytes < 1024:
155
+ return f"{size_bytes:.1f} {unit}"
156
+ size_bytes /= 1024
157
+ return f"{size_bytes:.1f} PB"
158
+
159
+
160
+ # ─── HTML Template ────────────────────────────────────────────────────────────
161
+
162
+ HTML = r"""
163
+ <!DOCTYPE html>
164
+ <html lang="en">
165
+ <head>
166
+ <meta charset="UTF-8" />
167
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
168
+ <title>⚑ SwiftLoad</title>
169
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap" rel="stylesheet" />
170
+ <style>
171
+ :root {
172
+ --bg: #0a0a0f;
173
+ --surface: #13131a;
174
+ --border: #1e1e2e;
175
+ --accent: #00f5a0;
176
+ --accent2: #00d4ff;
177
+ --danger: #ff4d6d;
178
+ --warn: #ffbe0b;
179
+ --text: #e8e8f0;
180
+ --muted: #5a5a7a;
181
+ --card: #0f0f1a;
182
+ }
183
+ * { box-sizing: border-box; margin: 0; padding: 0; }
184
+ body {
185
+ background: var(--bg);
186
+ color: var(--text);
187
+ font-family: 'Syne', sans-serif;
188
+ min-height: 100vh;
189
+ overflow-x: hidden;
190
+ }
191
+
192
+ /* Grid bg */
193
+ body::before {
194
+ content: '';
195
+ position: fixed; inset: 0;
196
+ background-image:
197
+ linear-gradient(rgba(0,245,160,0.03) 1px, transparent 1px),
198
+ linear-gradient(90deg, rgba(0,245,160,0.03) 1px, transparent 1px);
199
+ background-size: 40px 40px;
200
+ pointer-events: none;
201
+ z-index: 0;
202
+ }
203
+
204
+ .container {
205
+ max-width: 860px;
206
+ margin: 0 auto;
207
+ padding: 48px 24px 80px;
208
+ position: relative;
209
+ z-index: 1;
210
+ }
211
+
212
+ header {
213
+ text-align: center;
214
+ margin-bottom: 52px;
215
+ }
216
+ .logo {
217
+ font-size: 52px;
218
+ font-weight: 800;
219
+ letter-spacing: -2px;
220
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
221
+ -webkit-background-clip: text;
222
+ -webkit-text-fill-color: transparent;
223
+ background-clip: text;
224
+ line-height: 1;
225
+ }
226
+ .tagline {
227
+ color: var(--muted);
228
+ font-family: 'Space Mono', monospace;
229
+ font-size: 12px;
230
+ letter-spacing: 3px;
231
+ text-transform: uppercase;
232
+ margin-top: 8px;
233
+ }
234
+
235
+ .input-section {
236
+ background: var(--surface);
237
+ border: 1px solid var(--border);
238
+ border-radius: 16px;
239
+ padding: 28px;
240
+ margin-bottom: 32px;
241
+ position: relative;
242
+ overflow: hidden;
243
+ }
244
+ .input-section::before {
245
+ content: '';
246
+ position: absolute;
247
+ top: 0; left: 0; right: 0;
248
+ height: 2px;
249
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
250
+ }
251
+
252
+ .input-row {
253
+ display: flex;
254
+ gap: 12px;
255
+ }
256
+ input[type="text"] {
257
+ flex: 1;
258
+ background: var(--bg);
259
+ border: 1px solid var(--border);
260
+ border-radius: 10px;
261
+ padding: 14px 18px;
262
+ color: var(--text);
263
+ font-family: 'Space Mono', monospace;
264
+ font-size: 13px;
265
+ outline: none;
266
+ transition: border-color 0.2s;
267
+ }
268
+ input[type="text"]:focus {
269
+ border-color: var(--accent);
270
+ box-shadow: 0 0 0 3px rgba(0,245,160,0.08);
271
+ }
272
+ input[type="text"]::placeholder { color: var(--muted); }
273
+
274
+ button.download-btn {
275
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
276
+ color: #000;
277
+ border: none;
278
+ border-radius: 10px;
279
+ padding: 14px 28px;
280
+ font-family: 'Syne', sans-serif;
281
+ font-weight: 700;
282
+ font-size: 14px;
283
+ letter-spacing: 0.5px;
284
+ cursor: pointer;
285
+ transition: opacity 0.2s, transform 0.1s;
286
+ white-space: nowrap;
287
+ }
288
+ button.download-btn:hover { opacity: 0.9; transform: translateY(-1px); }
289
+ button.download-btn:active { transform: translateY(0); }
290
+ button.download-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
291
+
292
+ /* Active Downloads */
293
+ #active-downloads { margin-bottom: 32px; }
294
+
295
+ .section-title {
296
+ font-family: 'Space Mono', monospace;
297
+ font-size: 11px;
298
+ letter-spacing: 3px;
299
+ text-transform: uppercase;
300
+ color: var(--muted);
301
+ margin-bottom: 14px;
302
+ }
303
+
304
+ .dl-card {
305
+ background: var(--card);
306
+ border: 1px solid var(--border);
307
+ border-radius: 12px;
308
+ padding: 20px;
309
+ margin-bottom: 12px;
310
+ animation: slideIn 0.3s ease;
311
+ }
312
+ @keyframes slideIn {
313
+ from { opacity: 0; transform: translateY(-10px); }
314
+ to { opacity: 1; transform: translateY(0); }
315
+ }
316
+
317
+ .dl-header {
318
+ display: flex;
319
+ justify-content: space-between;
320
+ align-items: flex-start;
321
+ margin-bottom: 12px;
322
+ gap: 12px;
323
+ }
324
+ .dl-filename {
325
+ font-weight: 700;
326
+ font-size: 14px;
327
+ word-break: break-all;
328
+ flex: 1;
329
+ }
330
+ .dl-badge {
331
+ font-family: 'Space Mono', monospace;
332
+ font-size: 10px;
333
+ padding: 3px 10px;
334
+ border-radius: 20px;
335
+ white-space: nowrap;
336
+ font-weight: 700;
337
+ }
338
+ .badge-downloading { background: rgba(0,212,255,0.15); color: var(--accent2); border: 1px solid rgba(0,212,255,0.3); }
339
+ .badge-done { background: rgba(0,245,160,0.12); color: var(--accent); border: 1px solid rgba(0,245,160,0.3); }
340
+ .badge-error { background: rgba(255,77,109,0.12); color: var(--danger); border: 1px solid rgba(255,77,109,0.3); }
341
+
342
+ .progress-track {
343
+ background: var(--border);
344
+ border-radius: 99px;
345
+ height: 6px;
346
+ overflow: hidden;
347
+ margin-bottom: 10px;
348
+ }
349
+ .progress-fill {
350
+ height: 100%;
351
+ border-radius: 99px;
352
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
353
+ transition: width 0.4s ease;
354
+ position: relative;
355
+ }
356
+ .progress-fill.done { background: var(--accent); }
357
+ .progress-fill.error { background: var(--danger); }
358
+ .progress-fill::after {
359
+ content: '';
360
+ position: absolute;
361
+ right: 0; top: 0; bottom: 0;
362
+ width: 20px;
363
+ background: rgba(255,255,255,0.4);
364
+ border-radius: 99px;
365
+ filter: blur(4px);
366
+ animation: shimmer 1s ease-in-out infinite;
367
+ }
368
+ .progress-fill.done::after, .progress-fill.error::after { display: none; }
369
+ @keyframes shimmer {
370
+ 0%, 100% { opacity: 0; }
371
+ 50% { opacity: 1; }
372
+ }
373
+
374
+ .dl-meta {
375
+ display: flex;
376
+ gap: 16px;
377
+ font-family: 'Space Mono', monospace;
378
+ font-size: 11px;
379
+ color: var(--muted);
380
+ flex-wrap: wrap;
381
+ }
382
+ .dl-meta span { display: flex; align-items: center; gap: 4px; }
383
+ .dl-meta .highlight { color: var(--accent2); }
384
+
385
+ .dl-actions { margin-top: 12px; display: flex; gap: 8px; }
386
+ .btn-sm {
387
+ font-family: 'Space Mono', monospace;
388
+ font-size: 11px;
389
+ padding: 6px 14px;
390
+ border-radius: 6px;
391
+ border: 1px solid var(--border);
392
+ background: transparent;
393
+ color: var(--text);
394
+ cursor: pointer;
395
+ text-decoration: none;
396
+ transition: all 0.15s;
397
+ display: inline-block;
398
+ }
399
+ .btn-sm:hover { border-color: var(--accent); color: var(--accent); }
400
+ .btn-sm.primary { border-color: var(--accent); color: var(--accent); background: rgba(0,245,160,0.05); }
401
+ .btn-sm.danger { border-color: var(--danger); color: var(--danger); }
402
+
403
+ .error-msg {
404
+ font-family: 'Space Mono', monospace;
405
+ font-size: 11px;
406
+ color: var(--danger);
407
+ margin-top: 8px;
408
+ background: rgba(255,77,109,0.08);
409
+ padding: 8px 12px;
410
+ border-radius: 6px;
411
+ border-left: 3px solid var(--danger);
412
+ }
413
+
414
+ /* History */
415
+ #history-section {}
416
+ .history-card {
417
+ background: var(--card);
418
+ border: 1px solid var(--border);
419
+ border-radius: 12px;
420
+ padding: 16px 20px;
421
+ margin-bottom: 10px;
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 16px;
425
+ transition: border-color 0.15s;
426
+ }
427
+ .history-card:hover { border-color: var(--muted); }
428
+ .hist-icon {
429
+ width: 36px; height: 36px;
430
+ border-radius: 8px;
431
+ background: rgba(0,245,160,0.1);
432
+ border: 1px solid rgba(0,245,160,0.2);
433
+ display: flex; align-items: center; justify-content: center;
434
+ font-size: 16px;
435
+ flex-shrink: 0;
436
+ }
437
+ .hist-info { flex: 1; min-width: 0; }
438
+ .hist-name {
439
+ font-weight: 700;
440
+ font-size: 13px;
441
+ white-space: nowrap;
442
+ overflow: hidden;
443
+ text-overflow: ellipsis;
444
+ }
445
+ .hist-meta {
446
+ font-family: 'Space Mono', monospace;
447
+ font-size: 10px;
448
+ color: var(--muted);
449
+ margin-top: 3px;
450
+ }
451
+ .hist-actions { flex-shrink: 0; }
452
+
453
+ .toast {
454
+ position: fixed;
455
+ bottom: 24px; right: 24px;
456
+ background: var(--surface);
457
+ border: 1px solid var(--accent);
458
+ color: var(--accent);
459
+ font-family: 'Space Mono', monospace;
460
+ font-size: 12px;
461
+ padding: 12px 20px;
462
+ border-radius: 10px;
463
+ z-index: 999;
464
+ animation: toastIn 0.3s ease;
465
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
466
+ }
467
+ @keyframes toastIn {
468
+ from { opacity: 0; transform: translateY(20px); }
469
+ to { opacity: 1; transform: translateY(0); }
470
+ }
471
+
472
+ .empty { text-align: center; padding: 32px; color: var(--muted); font-family: 'Space Mono', monospace; font-size: 12px; }
473
+ </style>
474
+ </head>
475
+ <body>
476
+ <div class="container">
477
+ <header>
478
+ <div class="logo">⚑ SwiftLoad</div>
479
+ <div class="tagline">High-Speed File Downloader</div>
480
+ </header>
481
+
482
+ <div class="input-section">
483
+ <div class="input-row">
484
+ <input type="text" id="url-input" placeholder="Paste any URL to download..." autocomplete="off" />
485
+ <button class="download-btn" id="dl-btn" onclick="startDownload()">Download</button>
486
+ </div>
487
+ </div>
488
+
489
+ <div id="active-downloads"></div>
490
+
491
+ <div id="history-section">
492
+ <div class="section-title">Download History</div>
493
+ <div id="history-list"><div class="empty">No downloads yet</div></div>
494
+ </div>
495
+ </div>
496
+
497
+ <script>
498
+ const activeTasks = {};
499
+
500
+ async function startDownload() {
501
+ const input = document.getElementById('url-input');
502
+ const btn = document.getElementById('dl-btn');
503
+ const url = input.value.trim();
504
+ if (!url) return;
505
+
506
+ btn.disabled = true;
507
+ btn.textContent = 'Starting...';
508
+
509
+ try {
510
+ const res = await fetch('/download', {
511
+ method: 'POST',
512
+ headers: { 'Content-Type': 'application/json' },
513
+ body: JSON.stringify({ url }),
514
+ });
515
+ const data = await res.json();
516
+ if (data.error) { showToast('Error: ' + data.error, true); return; }
517
+
518
+ input.value = '';
519
+ addActiveCard(data.task_id, data.filename, url);
520
+ pollProgress(data.task_id);
521
+ showToast('Download started!');
522
+ } catch (e) {
523
+ showToast('Request failed', true);
524
+ } finally {
525
+ btn.disabled = false;
526
+ btn.textContent = 'Download';
527
+ }
528
+ }
529
+
530
+ document.getElementById('url-input').addEventListener('keydown', e => {
531
+ if (e.key === 'Enter') startDownload();
532
+ });
533
+
534
+ function addActiveCard(taskId, filename, url) {
535
+ const container = document.getElementById('active-downloads');
536
+ if (!document.querySelector('.section-title.active-title')) {
537
+ const title = document.createElement('div');
538
+ title.className = 'section-title active-title';
539
+ title.textContent = 'Active Downloads';
540
+ container.prepend(title);
541
+ }
542
+ const card = document.createElement('div');
543
+ card.className = 'dl-card';
544
+ card.id = `task-${taskId}`;
545
+ card.innerHTML = `
546
+ <div class="dl-header">
547
+ <div class="dl-filename">${escapeHtml(filename)}</div>
548
+ <span class="dl-badge badge-downloading" id="badge-${taskId}">DOWNLOADING</span>
549
+ </div>
550
+ <div class="progress-track"><div class="progress-fill" id="fill-${taskId}" style="width:0%"></div></div>
551
+ <div class="dl-meta" id="meta-${taskId}">
552
+ <span>⚑ <span class="highlight" id="speed-${taskId}">connecting...</span></span>
553
+ <span>πŸ“¦ <span id="size-${taskId}">–</span></span>
554
+ <span>⏱ ETA: <span id="eta-${taskId}">–</span></span>
555
+ <span>πŸ”’ <span id="pct-${taskId}">0%</span></span>
556
+ </div>
557
+ <div id="err-${taskId}"></div>
558
+ <div class="dl-actions" id="actions-${taskId}"></div>
559
+ `;
560
+ container.appendChild(card);
561
+ activeTasks[taskId] = true;
562
+ }
563
+
564
+ async function pollProgress(taskId) {
565
+ while (activeTasks[taskId]) {
566
+ await new Promise(r => setTimeout(r, 800));
567
+ try {
568
+ const res = await fetch(`/progress/${taskId}`);
569
+ const data = await res.json();
570
+ updateCard(taskId, data);
571
+ if (data.status === 'done' || data.status === 'error') {
572
+ activeTasks[taskId] = false;
573
+ if (data.status === 'done') loadHistory();
574
+ }
575
+ } catch (e) { break; }
576
+ }
577
+ }
578
+
579
+ function updateCard(taskId, data) {
580
+ const fill = document.getElementById(`fill-${taskId}`);
581
+ const badge = document.getElementById(`badge-${taskId}`);
582
+ const speed = document.getElementById(`speed-${taskId}`);
583
+ const size = document.getElementById(`size-${taskId}`);
584
+ const eta = document.getElementById(`eta-${taskId}`);
585
+ const pct = document.getElementById(`pct-${taskId}`);
586
+ const errDiv = document.getElementById(`err-${taskId}`);
587
+ const actions = document.getElementById(`actions-${taskId}`);
588
+
589
+ const p = data.progress || 0;
590
+ if (fill) { fill.style.width = p + '%'; }
591
+ if (pct) pct.textContent = p + '%';
592
+ if (speed) speed.textContent = data.speed || (data.status === 'downloading' ? 'connecting...' : '–');
593
+ if (size && data.size) size.textContent = data.size;
594
+ if (eta) eta.textContent = data.eta || '–';
595
+
596
+ if (data.status === 'done') {
597
+ if (badge) { badge.textContent = 'DONE'; badge.className = 'dl-badge badge-done'; }
598
+ if (fill) { fill.classList.add('done'); fill.style.width = '100%'; }
599
+ if (actions) {
600
+ actions.innerHTML = `<a class="btn-sm primary" href="/dl/${taskId}" download>⬇ Download File</a>`;
601
+ }
602
+ showToast('Download complete: ' + (data.filename || ''));
603
+ } else if (data.status === 'error') {
604
+ if (badge) { badge.textContent = 'ERROR'; badge.className = 'dl-badge badge-error'; }
605
+ if (fill) { fill.classList.add('error'); }
606
+ if (errDiv && data.error) {
607
+ errDiv.innerHTML = `<div class="error-msg">⚠ ${escapeHtml(data.error)}</div>`;
608
+ }
609
+ showToast('Download failed', true);
610
+ }
611
+ }
612
+
613
+ async function loadHistory() {
614
+ const res = await fetch('/history');
615
+ const data = await res.json();
616
+ const list = document.getElementById('history-list');
617
+ if (!data.length) { list.innerHTML = '<div class="empty">No downloads yet</div>'; return; }
618
+ list.innerHTML = data.map(item => `
619
+ <div class="history-card">
620
+ <div class="hist-icon">${getFileIcon(item.filename)}</div>
621
+ <div class="hist-info">
622
+ <div class="hist-name" title="${escapeHtml(item.filename)}">${escapeHtml(item.filename)}</div>
623
+ <div class="hist-meta">${item.size || '–'} &nbsp;Β·&nbsp; ${item.timestamp}</div>
624
+ </div>
625
+ <div class="hist-actions">
626
+ <a class="btn-sm primary" href="/dl/${item.task_id}" download>⬇ Download</a>
627
+ </div>
628
+ </div>
629
+ `).join('');
630
+ }
631
+
632
+ function getFileIcon(name) {
633
+ const ext = name.split('.').pop().toLowerCase();
634
+ if (['mp4','mkv','avi','mov','webm'].includes(ext)) return '🎬';
635
+ if (['mp3','wav','flac','ogg'].includes(ext)) return '🎡';
636
+ if (['jpg','jpeg','png','gif','webp'].includes(ext)) return 'πŸ–Ό';
637
+ if (['zip','tar','gz','rar','7z'].includes(ext)) return 'πŸ“¦';
638
+ if (['pdf'].includes(ext)) return 'πŸ“„';
639
+ if (['exe','dmg','deb','apk'].includes(ext)) return 'βš™';
640
+ return 'πŸ“';
641
+ }
642
+
643
+ function escapeHtml(str) {
644
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
645
+ }
646
+
647
+ function showToast(msg, isErr = false) {
648
+ const t = document.createElement('div');
649
+ t.className = 'toast';
650
+ if (isErr) t.style.borderColor = 'var(--danger)', t.style.color = 'var(--danger)';
651
+ t.textContent = msg;
652
+ document.body.appendChild(t);
653
+ setTimeout(() => t.remove(), 3500);
654
+ }
655
+
656
+ loadHistory();
657
+ </script>
658
+ </body>
659
+ </html>
660
+ """
661
+
662
+
663
+ # ─── Routes ────────────────────────────────��──────────────────────────────────
664
+
665
+ @app.route("/")
666
+ def index():
667
+ return render_template_string(HTML)
668
+
669
+
670
+ @app.route("/download", methods=["POST"])
671
+ def start_download():
672
+ data = request.get_json()
673
+ url = (data or {}).get("url", "").strip()
674
+ if not url:
675
+ return jsonify({"error": "No URL provided"}), 400
676
+ if not url.startswith(("http://", "https://", "ftp://")):
677
+ return jsonify({"error": "Invalid URL. Must start with http://, https://, or ftp://"}), 400
678
+
679
+ task_id = uuid.uuid4().hex
680
+ filename = get_filename_from_url(url)
681
+
682
+ # Ensure unique filename
683
+ save_path = DOWNLOAD_DIR / filename
684
+ counter = 1
685
+ stem = save_path.stem
686
+ suffix = save_path.suffix
687
+ while save_path.exists():
688
+ save_path = DOWNLOAD_DIR / f"{stem}_{counter}{suffix}"
689
+ counter += 1
690
+
691
+ thread = threading.Thread(
692
+ target=download_with_wget,
693
+ args=(task_id, url, save_path),
694
+ daemon=True,
695
+ )
696
+ thread.start()
697
+
698
+ return jsonify({"task_id": task_id, "filename": save_path.name})
699
+
700
+
701
+ @app.route("/progress/<task_id>")
702
+ def get_progress(task_id):
703
+ data = progress_store.get(task_id)
704
+ if not data:
705
+ return jsonify({"status": "not_found"}), 404
706
+ return jsonify(data)
707
+
708
+
709
+ @app.route("/dl/<task_id>")
710
+ def download_file(task_id):
711
+ entry = progress_store.get(task_id)
712
+ if not entry or entry.get("status") != "done":
713
+ # Try from history
714
+ history = load_history()
715
+ for h in history:
716
+ if h["task_id"] == task_id:
717
+ p = Path(h["path"])
718
+ if p.exists():
719
+ return send_from_directory(p.parent.resolve(), p.name, as_attachment=True)
720
+ return jsonify({"error": "File not found or not ready"}), 404
721
+ filename = entry["filename"]
722
+ return send_from_directory(DOWNLOAD_DIR.resolve(), filename, as_attachment=True)
723
+
724
+
725
+ @app.route("/history")
726
+ def get_history():
727
+ return jsonify(load_history())
728
+
729
+
730
+ if __name__ == "__main__":
731
+ print("⚑ SwiftLoad running at http://localhost:5000")
732
+ app.run(debug=True, host="0.0.0.0", port=5000, threaded=True)
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ flask>=2.3.0