hannabaker commited on
Commit
46e5393
·
verified ·
1 Parent(s): 26ce258

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +172 -894
index.html CHANGED
@@ -5,955 +5,233 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>VOD Archiver Pro</title>
7
  <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
  :root {
15
- --bg-primary: #0f0f23;
16
- --bg-secondary: #1a1a2e;
17
- --bg-tertiary: #252542;
18
- --accent: #6366f1;
19
- --accent-hover: #4f46e5;
20
- --success: #10b981;
21
- --error: #ef4444;
22
- --warning: #f59e0b;
23
- --text-primary: #ffffff;
24
- --text-secondary: #a0a0c0;
25
- --border: #2d2d4a;
26
- --shadow: rgba(0, 0, 0, 0.3);
27
  }
28
-
29
  body {
30
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
31
- background: var(--bg-primary);
32
- color: var(--text-primary);
33
- min-height: 100vh;
34
- display: flex;
35
- align-items: center;
36
- justify-content: center;
37
- padding: 1rem;
38
- background-image:
39
- radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
40
- radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.1) 0%, transparent 50%),
41
- radial-gradient(circle at 40% 40%, rgba(245, 158, 11, 0.05) 0%, transparent 50%);
42
  }
43
-
44
  .container {
45
- width: 100%;
46
- max-width: 900px;
47
- background: var(--bg-secondary);
48
- border-radius: 20px;
49
- box-shadow: 0 20px 60px var(--shadow);
50
- overflow: hidden;
51
- border: 1px solid var(--border);
52
  }
53
-
54
  .header {
55
- background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
56
- padding: 2.5rem;
57
- text-align: center;
58
  border-bottom: 1px solid var(--border);
59
- position: relative;
60
- overflow: hidden;
61
- }
62
-
63
- .header::before {
64
- content: '';
65
- position: absolute;
66
- top: -50%;
67
- left: -50%;
68
- width: 200%;
69
- height: 200%;
70
- background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
71
- animation: rotate 30s linear infinite;
72
- }
73
-
74
- @keyframes rotate {
75
- 0% { transform: rotate(0deg); }
76
- 100% { transform: rotate(360deg); }
77
- }
78
-
79
- .header h1 {
80
- font-size: 2.5rem;
81
- font-weight: 800;
82
- background: linear-gradient(135deg, var(--accent) 0%, var(--success) 100%);
83
- -webkit-background-clip: text;
84
- -webkit-text-fill-color: transparent;
85
- margin-bottom: 0.5rem;
86
- position: relative;
87
- z-index: 1;
88
- }
89
-
90
- .header p {
91
- color: var(--text-secondary);
92
- font-size: 1rem;
93
- position: relative;
94
- z-index: 1;
95
- }
96
-
97
- .content {
98
- padding: 2.5rem;
99
- }
100
-
101
- .form-section {
102
- margin-bottom: 2rem;
103
- }
104
-
105
- .input-group {
106
- margin-bottom: 1.5rem;
107
- }
108
-
109
- .input-group label {
110
- display: block;
111
- margin-bottom: 0.5rem;
112
- font-weight: 600;
113
- color: var(--text-primary);
114
- font-size: 0.9rem;
115
- text-transform: uppercase;
116
- letter-spacing: 0.5px;
117
- }
118
-
119
- .input-wrapper {
120
- position: relative;
121
- }
122
-
123
- .input-wrapper input,
124
- .input-wrapper select {
125
- width: 100%;
126
- padding: 1rem 1.25rem;
127
- background: var(--bg-tertiary);
128
- border: 2px solid var(--border);
129
- border-radius: 12px;
130
- color: var(--text-primary);
131
- font-size: 1rem;
132
- transition: all 0.3s ease;
133
- }
134
-
135
- .input-wrapper input:focus,
136
- .input-wrapper select:focus {
137
- outline: none;
138
- border-color: var(--accent);
139
- box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
140
- transform: translateY(-2px);
141
  }
142
-
143
- .input-wrapper .icon {
144
- position: absolute;
145
- right: 1.25rem;
146
- top: 50%;
147
- transform: translateY(-50%);
148
- color: var(--text-secondary);
149
- pointer-events: none;
150
- }
151
-
152
- .quality-selector {
153
- display: none;
154
- margin-bottom: 1.5rem;
155
- animation: slideDown 0.3s ease-out;
156
- }
157
-
158
- @keyframes slideDown {
159
- from {
160
- opacity: 0;
161
- transform: translateY(-10px);
162
- }
163
- to {
164
- opacity: 1;
165
- transform: translateY(0);
166
- }
167
- }
168
-
169
- .button-group {
170
- display: grid;
171
- grid-template-columns: 2fr 1fr 1fr;
172
- gap: 1rem;
173
- margin-bottom: 2rem;
174
- }
175
-
176
  button {
177
- padding: 1rem 1.5rem;
178
- font-size: 1rem;
179
- font-weight: 600;
180
- border: none;
181
- border-radius: 12px;
182
- cursor: pointer;
183
- transition: all 0.3s ease;
184
- position: relative;
185
- overflow: hidden;
186
- }
187
-
188
- button::before {
189
- content: '';
190
- position: absolute;
191
- top: 50%;
192
- left: 50%;
193
- width: 0;
194
- height: 0;
195
- background: rgba(255, 255, 255, 0.1);
196
- border-radius: 50%;
197
- transform: translate(-50%, -50%);
198
- transition: width 0.6s, height 0.6s;
199
- }
200
-
201
- button:active::before {
202
- width: 300px;
203
- height: 300px;
204
- }
205
-
206
- .btn-primary {
207
- background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
208
- color: white;
209
- box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
210
- }
211
-
212
- .btn-primary:hover:not(:disabled) {
213
- transform: translateY(-2px);
214
- box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
215
- }
216
-
217
- .btn-secondary {
218
- background: var(--bg-tertiary);
219
- color: var(--text-primary);
220
- border: 2px solid var(--border);
221
- }
222
-
223
- .btn-secondary:hover:not(:disabled) {
224
- background: var(--border);
225
- transform: translateY(-2px);
226
- }
227
-
228
- button:disabled {
229
- opacity: 0.5;
230
- cursor: not-allowed;
231
- transform: none !important;
232
- }
233
-
234
- .status-section {
235
- display: none;
236
- background: var(--bg-tertiary);
237
- border-radius: 16px;
238
- padding: 2rem;
239
- margin-bottom: 2rem;
240
- border: 1px solid var(--border);
241
- animation: fadeIn 0.5s ease-out;
242
- }
243
-
244
- @keyframes fadeIn {
245
- from {
246
- opacity: 0;
247
- transform: translateY(20px);
248
- }
249
- to {
250
- opacity: 1;
251
- transform: translateY(0);
252
- }
253
- }
254
-
255
- .status-header {
256
- display: flex;
257
- align-items: center;
258
- justify-content: space-between;
259
- margin-bottom: 1.5rem;
260
- }
261
-
262
- .status-title {
263
- font-size: 1.25rem;
264
- font-weight: 600;
265
- display: flex;
266
- align-items: center;
267
- gap: 0.75rem;
268
- }
269
-
270
- .status-indicator {
271
- width: 12px;
272
- height: 12px;
273
- border-radius: 50%;
274
- background: var(--warning);
275
- animation: pulse 2s infinite;
276
- }
277
-
278
- .status-indicator.success {
279
- background: var(--success);
280
- animation: none;
281
- }
282
-
283
- .status-indicator.error {
284
- background: var(--error);
285
- animation: none;
286
- }
287
-
288
- @keyframes pulse {
289
- 0%, 100% { opacity: 1; transform: scale(1); }
290
- 50% { opacity: 0.5; transform: scale(0.8); }
291
- }
292
-
293
- .progress-container {
294
- margin-bottom: 1.5rem;
295
- }
296
-
297
- .progress-info {
298
- display: flex;
299
- justify-content: space-between;
300
- margin-bottom: 0.5rem;
301
- font-size: 0.9rem;
302
- }
303
-
304
- .progress-bar {
305
- width: 100%;
306
- height: 8px;
307
- background: var(--bg-primary);
308
- border-radius: 4px;
309
- overflow: hidden;
310
- position: relative;
311
- }
312
-
313
- .progress-fill {
314
- height: 100%;
315
- background: linear-gradient(90deg, var(--accent) 0%, var(--success) 100%);
316
- width: 0%;
317
- transition: width 0.5s ease;
318
- position: relative;
319
- overflow: hidden;
320
- }
321
-
322
- .progress-fill::after {
323
- content: '';
324
- position: absolute;
325
- top: 0;
326
- left: 0;
327
- bottom: 0;
328
- right: 0;
329
- background: linear-gradient(
330
- 90deg,
331
- transparent,
332
- rgba(255, 255, 255, 0.2),
333
- transparent
334
- );
335
- animation: shimmer 2s infinite;
336
- }
337
-
338
- @keyframes shimmer {
339
- 0% { transform: translateX(-100%); }
340
- 100% { transform: translateX(100%); }
341
- }
342
-
343
  .log-container {
344
- background: var(--bg-primary);
345
- border-radius: 12px;
346
- padding: 1rem;
347
- height: 250px;
348
- overflow-y: auto;
349
- font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
350
- font-size: 0.875rem;
351
- line-height: 1.6;
352
- border: 1px solid var(--border);
353
- position: relative;
354
- }
355
-
356
- .log-controls {
357
- position: absolute;
358
- top: 0.5rem;
359
- right: 0.5rem;
360
- display: flex;
361
- gap: 0.5rem;
362
- z-index: 10;
363
- }
364
-
365
- .log-control-btn {
366
- padding: 0.25rem 0.5rem;
367
- background: var(--bg-tertiary);
368
- border: 1px solid var(--border);
369
- border-radius: 6px;
370
- color: var(--text-secondary);
371
- font-size: 0.75rem;
372
- cursor: pointer;
373
- transition: all 0.2s;
374
- }
375
-
376
- .log-control-btn:hover {
377
- background: var(--border);
378
- color: var(--text-primary);
379
- }
380
-
381
- .log-control-btn.active {
382
- background: var(--accent);
383
- color: white;
384
- border-color: var(--accent);
385
- }
386
-
387
- .log-entry {
388
- margin-bottom: 0.5rem;
389
- padding: 0.25rem 0;
390
- animation: slideIn 0.3s ease-out;
391
- word-wrap: break-word;
392
- }
393
-
394
- @keyframes slideIn {
395
- from {
396
- opacity: 0;
397
- transform: translateX(-10px);
398
- }
399
- to {
400
- opacity: 1;
401
- transform: translateX(0);
402
- }
403
- }
404
-
405
- .log-entry.error { color: var(--error); }
406
- .log-entry.success { color: var(--success); }
407
- .log-entry.info { color: var(--accent); }
408
- .log-entry.warning { color: var(--warning); }
409
-
410
- .stats-grid {
411
- display: grid;
412
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
413
- gap: 1rem;
414
- margin-top: 1.5rem;
415
- }
416
-
417
- .stat-card {
418
- background: var(--bg-primary);
419
- padding: 1.25rem;
420
- border-radius: 12px;
421
- text-align: center;
422
- border: 1px solid var(--border);
423
- transition: all 0.3s ease;
424
- }
425
-
426
- .stat-card:hover {
427
- transform: translateY(-2px);
428
- border-color: var(--accent);
429
- }
430
-
431
- .stat-value {
432
- font-size: 1.75rem;
433
- font-weight: 700;
434
- color: var(--accent);
435
- margin-bottom: 0.25rem;
436
- }
437
-
438
- .stat-label {
439
- font-size: 0.875rem;
440
- color: var(--text-secondary);
441
- text-transform: uppercase;
442
- letter-spacing: 0.5px;
443
- }
444
-
445
- .video-preview {
446
- display: none;
447
- margin-top: 2rem;
448
- background: var(--bg-tertiary);
449
- border-radius: 16px;
450
- padding: 1.5rem;
451
- border: 1px solid var(--border);
452
- }
453
-
454
- .video-preview h3 {
455
- margin-bottom: 1rem;
456
- color: var(--text-primary);
457
- }
458
-
459
- .video-player {
460
- width: 100%;
461
- border-radius: 12px;
462
- background: var(--bg-primary);
463
- }
464
-
465
- .download-section {
466
- display: flex;
467
- gap: 1rem;
468
- margin-top: 1rem;
469
- }
470
-
471
- /* Custom scrollbar */
472
- .log-container::-webkit-scrollbar {
473
- width: 8px;
474
- }
475
-
476
- .log-container::-webkit-scrollbar-track {
477
- background: var(--bg-secondary);
478
- border-radius: 4px;
479
- }
480
-
481
- .log-container::-webkit-scrollbar-thumb {
482
- background: var(--border);
483
- border-radius: 4px;
484
- }
485
-
486
- .log-container::-webkit-scrollbar-thumb:hover {
487
- background: var(--accent);
488
- }
489
-
490
- /* Loading animation */
491
- .loading {
492
- display: inline-block;
493
- width: 20px;
494
- height: 20px;
495
- border: 3px solid var(--border);
496
- border-radius: 50%;
497
- border-top-color: var(--accent);
498
- animation: spin 1s ease-in-out infinite;
499
- }
500
-
501
- @keyframes spin {
502
- to { transform: rotate(360deg); }
503
- }
504
-
505
- /* Responsive design */
506
- @media (max-width: 768px) {
507
- .container {
508
- margin: 1rem;
509
- }
510
-
511
- .header h1 {
512
- font-size: 2rem;
513
- }
514
-
515
- .button-group {
516
- grid-template-columns: 1fr;
517
- }
518
-
519
- .stats-grid {
520
- grid-template-columns: 1fr 1fr;
521
- }
522
- }
523
  </style>
524
  </head>
525
  <body>
526
-
527
  <div class="container">
528
- <div class="header">
529
- <h1>VOD Archiver Pro</h1>
530
- <p>Download and archive Twitch VODs with quality selection</p>
531
- </div>
532
-
533
  <div class="content">
534
- <div class="form-section">
535
- <form id="vodForm">
536
- <div class="input-group">
537
- <label for="vod_url">Twitch VOD URL</label>
538
- <div class="input-wrapper">
539
- <input type="text" id="vod_url" name="vod_url"
540
- placeholder="https://www.twitch.tv/videos/123456789" required>
541
- <span class="icon">🎬</span>
542
- </div>
543
  </div>
544
-
545
- <div class="quality-selector" id="qualitySelector">
546
- <label for="format_select">Select Quality</label>
547
- <div class="input-wrapper">
548
- <select id="format_select" name="format_select">
549
- <option value="best">Best Quality (Source)</option>
550
- </select>
551
- <span class="icon">🎯</span>
552
- </div>
553
- </div>
554
-
555
  <div class="input-group">
556
- <label for="mega_user">Mega.nz Email</label>
557
- <div class="input-wrapper">
558
- <input type="email" id="mega_user" name="mega_user"
559
- placeholder="your@email.com" required>
560
- <span class="icon">📧</span>
561
- </div>
562
  </div>
563
-
564
  <div class="input-group">
565
- <label for="mega_pass">Mega.nz Password</label>
566
- <div class="input-wrapper">
567
- <input type="password" id="mega_pass" name="mega_pass"
568
- placeholder="••••••••" required>
569
- <span class="icon">🔒</span>
570
- </div>
571
  </div>
572
-
573
- <div class="button-group">
574
- <button type="submit" class="btn-primary" id="submitBtn">
575
- <span id="btnText">Start Archiving</span>
576
- </button>
577
- <button type="button" class="btn-secondary" id="qualityBtn">
578
- Get Qualities
579
- </button>
580
- <button type="button" class="btn-secondary" id="validateBtn">
581
- Validate Login
582
- </button>
583
- </div>
584
- </form>
585
- </div>
586
-
587
- <div class="status-section" id="statusSection">
588
- <div class="status-header">
589
- <div class="status-title">
590
- <span id="statusText">Initializing</span>
591
- <div class="status-indicator" id="statusIndicator"></div>
592
  </div>
593
- <div class="loading" id="loadingSpinner" style="display: none;"></div>
594
  </div>
595
-
596
- <div class="progress-container" id="downloadProgress" style="display: none;">
597
- <div class="progress-info">
598
- <span>Download Progress</span>
599
- <span id="downloadInfo">0%</span>
600
- </div>
601
- <div class="progress-bar">
602
- <div class="progress-fill" id="downloadProgressFill"></div>
603
- </div>
604
  </div>
 
605
 
606
- <div class="progress-container" id="uploadProgress" style="display: none;">
607
- <div class="progress-info">
608
- <span>Upload Progress</span>
609
- <span id="uploadInfo">0%</span>
610
- </div>
611
- <div class="progress-bar">
612
- <div class="progress-fill" id="uploadProgressFill"></div>
613
- </div>
614
  </div>
615
-
616
  <div class="log-container" id="logContainer">
617
  <div class="log-controls">
618
- <button class="log-control-btn" id="clearLogsBtn">Clear</button>
619
- <button class="log-control-btn active" id="autoScrollBtn">Auto Scroll</button>
620
- </div>
621
- </div>
622
-
623
- <div class="stats-grid" id="statsGrid">
624
- <div class="stat-card">
625
- <div class="stat-value" id="downloadSpeed">--</div>
626
- <div class="stat-label">Download Speed</div>
627
- </div>
628
- <div class="stat-card">
629
- <div class="stat-value" id="fileSize">--</div>
630
- <div class="stat-label">File Size</div>
631
- </div>
632
- <div class="stat-card">
633
- <div class="stat-value" id="timeElapsed">00:00</div>
634
- <div class="stat-label">Time Elapsed</div>
635
- </div>
636
- <div class="stat-card">
637
- <div class="stat-value" id="eta">--</div>
638
- <div class="stat-label">ETA</div>
639
  </div>
640
  </div>
641
  </div>
642
 
643
- <div class="video-preview" id="videoPreview">
644
- <h3>Preview & Download</h3>
645
- <video class="video-player" id="videoPlayer" controls></video>
646
- <div class="download-section">
647
- <button class="btn-primary" id="downloadBtn">Download VOD</button>
648
- <button class="btn-secondary" id="copyLinkBtn">Copy Link</button>
 
649
  </div>
650
  </div>
651
  </div>
652
  </div>
653
 
654
  <script>
655
- // UI Elements
656
- const elements = {
657
- form: document.getElementById('vodForm'),
658
- vodUrl: document.getElementById('vod_url'),
659
- megaUser: document.getElementById('mega_user'),
660
- megaPass: document.getElementById('mega_pass'),
661
- formatSelect: document.getElementById('format_select'),
662
- qualitySelector: document.getElementById('qualitySelector'),
663
- submitBtn: document.getElementById('submitBtn'),
664
- qualityBtn: document.getElementById('qualityBtn'),
665
- validateBtn: document.getElementById('validateBtn'),
666
- btnText: document.getElementById('btnText'),
667
- statusSection: document.getElementById('statusSection'),
668
- statusText: document.getElementById('statusText'),
669
- statusIndicator: document.getElementById('statusIndicator'),
670
- loadingSpinner: document.getElementById('loadingSpinner'),
671
- logContainer: document.getElementById('logContainer'),
672
- clearLogsBtn: document.getElementById('clearLogsBtn'),
673
- autoScrollBtn: document.getElementById('autoScrollBtn'),
674
- downloadProgress: document.getElementById('downloadProgress'),
675
- downloadProgressFill: document.getElementById('downloadProgressFill'),
676
- downloadInfo: document.getElementById('downloadInfo'),
677
- uploadProgress: document.getElementById('uploadProgress'),
678
- uploadProgressFill: document.getElementById('uploadProgressFill'),
679
- uploadInfo: document.getElementById('uploadInfo'),
680
- downloadSpeed: document.getElementById('downloadSpeed'),
681
- fileSize: document.getElementById('fileSize'),
682
- timeElapsed: document.getElementById('timeElapsed'),
683
- eta: document.getElementById('eta'),
684
- videoPreview: document.getElementById('videoPreview'),
685
- videoPlayer: document.getElementById('videoPlayer'),
686
- downloadBtn: document.getElementById('downloadBtn'),
687
- copyLinkBtn: document.getElementById('copyLinkBtn')
688
  };
689
 
690
- // State
691
- let eventSource = null;
692
- let startTime = null;
693
- let autoScroll = true;
694
- let progressInterval = null;
695
- let currentFileInfo = null;
696
-
697
- // Auto scroll toggle
698
- elements.autoScrollBtn.addEventListener('click', () => {
699
- autoScroll = !autoScroll;
700
- elements.autoScrollBtn.classList.toggle('active', autoScroll);
701
- });
702
 
703
- // Clear logs
704
- elements.clearLogsBtn.addEventListener('click', () => {
705
- elements.logContainer.innerHTML = '';
706
- // Re-add controls
707
- elements.logContainer.appendChild(document.querySelector('.log-controls'));
708
- });
709
-
710
- // Add log entry
711
- function addLog(message, type = '') {
712
- const entry = document.createElement('div');
713
- entry.className = `log-entry ${type}`;
714
- entry.textContent = message;
715
- elements.logContainer.appendChild(entry);
716
-
717
- if (autoScroll) {
718
- elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
719
- }
720
- }
721
 
722
- // Update progress
723
- function updateProgress(phase, percent, info = {}) {
724
- if (phase === 'download') {
725
- elements.downloadProgress.style.display = 'block';
726
- elements.downloadProgressFill.style.width = `${percent}%`;
727
-
728
- let infoText = `${percent.toFixed(1)}%`;
729
- if (info.speed) infoText += ` @ ${info.speed}`;
730
- if (info.eta) infoText += ` (ETA: ${info.eta})`;
731
- elements.downloadInfo.textContent = infoText;
732
-
733
- if (info.speed) elements.downloadSpeed.textContent = info.speed;
734
- if (info.size) elements.fileSize.textContent = info.size;
735
- if (info.eta) elements.eta.textContent = info.eta;
736
- } else if (phase === 'upload') {
737
- elements.uploadProgress.style.display = 'block';
738
- elements.uploadProgressFill.style.width = `${percent}%`;
739
- elements.uploadInfo.textContent = `${percent}%`;
740
- }
741
  }
742
 
743
- // Update elapsed time
744
- function updateElapsedTime() {
745
- if (startTime) {
746
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
747
- const minutes = Math.floor(elapsed / 60);
748
- const seconds = elapsed % 60;
749
- elements.timeElapsed.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
750
- }
 
 
 
 
 
 
 
 
 
751
  }
752
 
753
- // Get quality options
754
- elements.qualityBtn.addEventListener('click', async () => {
755
- const vodUrl = elements.vodUrl.value.trim();
756
- if (!vodUrl) {
757
- alert('Please enter a VOD URL first');
758
- return;
759
- }
760
-
761
- elements.qualityBtn.disabled = true;
762
- elements.qualityBtn.textContent = 'Loading...';
763
-
764
  try {
765
- const response = await fetch(`/get_formats/${encodeURIComponent(vodUrl)}`);
766
- const data = await response.json();
767
-
768
- if (data.error) {
769
- throw new Error(data.error);
 
 
 
 
 
 
 
770
  }
 
 
771
 
772
- // Populate format selector
773
- elements.formatSelect.innerHTML = '';
774
- data.formats.forEach(format => {
775
- const option = document.createElement('option');
776
- option.value = format.format_id;
777
- option.textContent = format.label;
778
- elements.formatSelect.appendChild(option);
779
- });
780
-
781
- elements.qualitySelector.style.display = 'block';
782
- addLog('✅ Quality options loaded', 'success');
783
- } catch (error) {
784
- addLog(`❌ Failed to load qualities: ${error.message}`, 'error');
785
- } finally {
786
- elements.qualityBtn.disabled = false;
787
- elements.qualityBtn.textContent = 'Get Qualities';
788
- }
789
- });
790
-
791
- // Validate credentials
792
- elements.validateBtn.addEventListener('click', async () => {
793
- const megaUser = elements.megaUser.value.trim();
794
- const megaPass = elements.megaPass.value.trim();
795
-
796
- if (!megaUser || !megaPass) {
797
- alert('Please enter Mega credentials first');
798
- return;
799
- }
800
-
801
- elements.validateBtn.disabled = true;
802
- elements.validateBtn.textContent = 'Validating...';
803
-
804
- try {
805
- const response = await fetch(`/validate?mega_user=${encodeURIComponent(megaUser)}&mega_pass=${encodeURIComponent(megaPass)}`);
806
- const data = await response.json();
807
-
808
- if (data.valid) {
809
- addLog('✅ ' + data.message, 'success');
810
- } else {
811
- addLog('❌ ' + data.message, 'error');
812
- }
813
- } catch (error) {
814
- addLog('❌ Failed to validate credentials', 'error');
815
- } finally {
816
- elements.validateBtn.disabled = false;
817
- elements.validateBtn.textContent = 'Validate Login';
818
- }
819
- });
820
-
821
- // Main form submission
822
- elements.form.addEventListener('submit', (e) => {
823
- e.preventDefault();
824
-
825
- const vodUrl = elements.vodUrl.value.trim();
826
- const megaUser = elements.megaUser.value.trim();
827
- const megaPass = elements.megaPass.value.trim();
828
- const formatId = elements.formatSelect.value || 'best';
829
-
830
- if (!vodUrl || !megaUser || !megaPass) {
831
- alert('Please fill in all required fields');
832
- return;
833
- }
834
-
835
- // Reset UI
836
- startTime = Date.now();
837
- elements.statusSection.style.display = 'block';
838
- elements.statusText.textContent = 'Initializing';
839
- elements.statusIndicator.className = 'status-indicator';
840
- elements.loadingSpinner.style.display = 'block';
841
- elements.submitBtn.disabled = true;
842
- elements.qualityBtn.disabled = true;
843
- elements.validateBtn.disabled = true;
844
- elements.btnText.textContent = 'Processing...';
845
- elements.downloadProgress.style.display = 'none';
846
- elements.uploadProgress.style.display = 'none';
847
- elements.downloadProgressFill.style.width = '0%';
848
- elements.uploadProgressFill.style.width = '0%';
849
- elements.videoPreview.style.display = 'none';
850
-
851
- // Clear stats
852
- elements.downloadSpeed.textContent = '--';
853
- elements.fileSize.textContent = '--';
854
- elements.eta.textContent = '--';
855
-
856
- // Start elapsed time update
857
- if (progressInterval) clearInterval(progressInterval);
858
- progressInterval = setInterval(updateElapsedTime, 1000);
859
-
860
- // Close existing connection
861
- if (eventSource) {
862
- eventSource.close();
863
- }
864
-
865
- // Start new connection
866
- const params = new URLSearchParams({
867
- vod_url: vodUrl,
868
- mega_user: megaUser,
869
- mega_pass: megaPass,
870
- format_id: formatId
871
- });
872
-
873
- eventSource = new EventSource(`/process?${params.toString()}`);
874
-
875
- eventSource.onmessage = (event) => {
876
- try {
877
- const data = JSON.parse(event.data);
878
-
879
- switch (data.type) {
880
- case 'info':
881
- addLog(data.message, 'info');
882
- break;
883
-
884
- case 'phase':
885
- if (data.phase === 'download') {
886
- elements.statusText.textContent = 'Downloading VOD';
887
- } else if (data.phase === 'upload') {
888
- elements.statusText.textContent = 'Uploading to Mega';
889
- }
890
- break;
891
-
892
- case 'progress':
893
- // Fetch detailed progress
894
- fetch('/progress')
895
- .then(res => res.json())
896
- .then(progressData => {
897
- if (data.phase === 'download') {
898
- updateProgress('download', data.percent, progressData.download);
899
- } else if (data.phase === 'upload') {
900
- updateProgress('upload', data.percent, progressData.upload);
901
- }
902
- });
903
- break;
904
-
905
- case 'file_info':
906
- currentFileInfo = data.data;
907
- // Could enable preview here if needed
908
- break;
909
-
910
- case 'error':
911
- addLog(data.message, 'error');
912
- elements.statusIndicator.classList.add('error');
913
- elements.statusText.textContent = 'Error';
914
- break;
915
 
916
- case 'complete':
917
- elements.statusText.textContent = 'Complete!';
918
- elements.statusIndicator.classList.add('success');
919
- elements.loadingSpinner.style.display = 'none';
920
- addLog('✨ Process completed successfully!', 'success');
921
- break;
 
 
922
 
923
- case 'done':
924
- eventSource.close();
925
- elements.submitBtn.disabled = false;
926
- elements.qualityBtn.disabled = false;
927
- elements.validateBtn.disabled = false;
928
- elements.btnText.textContent = 'Start Archiving';
929
- if (progressInterval) {
930
- clearInterval(progressInterval);
931
- }
932
- break;
933
- }
934
- } catch (error) {
935
- console.error('Error parsing message:', error);
936
  }
937
  };
 
938
 
939
- eventSource.onerror = () => {
940
- addLog('❌ Connection lost', 'error');
941
- eventSource.close();
942
- elements.submitBtn.disabled = false;
943
- elements.qualityBtn.disabled = false;
944
- elements.validateBtn.disabled = false;
945
- elements.btnText.textContent = 'Start Archiving';
946
- elements.statusIndicator.classList.add('error');
947
- elements.loadingSpinner.style.display = 'none';
948
- if (progressInterval) {
949
- clearInterval(progressInterval);
950
- }
951
- };
952
- });
953
 
954
- // Initialize
955
- elements.vodUrl.focus();
956
  </script>
957
-
958
  </body>
959
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>VOD Archiver Pro</title>
7
  <style>
 
 
 
 
 
 
8
  :root {
9
+ --bg-primary: #121212; --bg-secondary: #1E1E1E; --bg-tertiary: #2A2A2A;
10
+ --accent: #6C63FF; --accent-hover: #574BFF; --success: #1DB954; --error: #F44336;
11
+ --text-primary: #FFFFFF; --text-secondary: #B3B3B3; --border: #3A3A3A;
 
 
 
 
 
 
 
 
 
12
  }
13
+ * { margin: 0; padding: 0; box-sizing: border-box; }
14
  body {
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
16
+ background: var(--bg-primary); color: var(--text-primary);
17
+ display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 1rem;
 
 
 
 
 
 
 
 
 
18
  }
 
19
  .container {
20
+ width: 100%; max-width: 800px; background: var(--bg-secondary);
21
+ border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); overflow: hidden;
 
 
 
 
 
22
  }
 
23
  .header {
24
+ padding: 2rem; text-align: center; background: var(--bg-tertiary);
 
 
25
  border-bottom: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
+ .header h1 { font-size: 2rem; font-weight: 700; color: var(--accent); margin-bottom: 0.5rem; }
28
+ .header p { color: var(--text-secondary); }
29
+ .content { padding: 2rem; }
30
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem; }
31
+ .input-group { display: flex; flex-direction: column; }
32
+ .input-group.full-width { grid-column: 1 / -1; }
33
+ label { margin-bottom: 0.5rem; font-weight: 500; color: var(--text-secondary); }
34
+ input, select {
35
+ padding: 0.75rem; background: var(--bg-primary); border: 1px solid var(--border);
36
+ border-radius: 8px; color: var(--text-primary); font-size: 1rem; transition: all 0.2s;
37
+ }
38
+ input:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
39
+ .button-group { display: flex; gap: 1rem; margin-top: 1.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  button {
41
+ flex-grow: 1; padding: 0.8rem; font-size: 1rem; font-weight: 600; border: none;
42
+ border-radius: 8px; cursor: pointer; transition: all 0.2s;
43
+ }
44
+ .btn-primary { background: var(--accent); color: white; }
45
+ .btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
46
+ .btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); }
47
+ .btn-secondary:hover:not(:disabled) { background: #333; }
48
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
49
+ .status-section { display: none; margin-top: 2rem; }
50
+ .progress-container { margin-bottom: 1rem; display: none; }
51
+ .progress-info { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.9rem; }
52
+ .progress-bar { width: 100%; height: 8px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; }
53
+ .progress-fill { height: 100%; background: var(--accent); width: 0%; transition: width 0.3s ease; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  .log-container {
55
+ background: var(--bg-primary); border-radius: 8px; padding: 1rem; height: 200px;
56
+ overflow-y: auto; font-family: monospace; font-size: 0.9rem; position: relative; margin-top: 1rem;
57
+ }
58
+ .log-controls { position: absolute; top: 0.5rem; right: 0.5rem; display: flex; gap: 0.5rem; }
59
+ .log-btn { background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.5rem; cursor: pointer; }
60
+ .log-btn.active { background: var(--accent); color: white; }
61
+ .log-entry.error { color: var(--error); } .log-entry.success { color: var(--success); }
62
+ .video-controls { display: none; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--border); }
63
+ .video-controls h3 { margin-bottom: 1rem; }
64
+ video { width: 100%; border-radius: 8px; background: black; }
65
+ .video-button-group { display: flex; gap: 1rem; margin-top: 1rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </style>
67
  </head>
68
  <body>
 
69
  <div class="container">
70
+ <div class="header"><h1>VOD Archiver Pro</h1><p>Download, Preview, and Archive Twitch VODs</p></div>
 
 
 
 
71
  <div class="content">
72
+ <form id="mainForm">
73
+ <div class="form-grid">
74
+ <div class="input-group full-width">
75
+ <label for="vodUrl">Twitch VOD URL</label>
76
+ <input type="text" id="vodUrl" placeholder="https://www.twitch.tv/videos/..." required>
 
 
 
 
77
  </div>
 
 
 
 
 
 
 
 
 
 
 
78
  <div class="input-group">
79
+ <label for="megaUser">Mega.nz Email</label>
80
+ <input type="email" id="megaUser" required>
 
 
 
 
81
  </div>
 
82
  <div class="input-group">
83
+ <label for="megaPass">Mega.nz Password</label>
84
+ <input type="password" id="megaPass" required>
 
 
 
 
85
  </div>
86
+ <div class="input-group full-width">
87
+ <label for="formatSelect">Quality</label>
88
+ <select id="formatSelect" disabled><option>Click "Get Qualities" first</option></select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
 
90
  </div>
91
+ <div class="button-group">
92
+ <button type="button" class="btn-secondary" id="qualityBtn">Get Qualities</button>
93
+ <button type="submit" class="btn-primary" id="startBtn" disabled>Start Archiving</button>
 
 
 
 
 
 
94
  </div>
95
+ </form>
96
 
97
+ <div class="status-section" id="statusSection">
98
+ <div class="progress-container" id="downloadProgress">
99
+ <div class="progress-info"><span>Download</span><span id="downloadInfo"></span></div>
100
+ <div class="progress-bar"><div class="progress-fill" id="downloadFill"></div></div>
101
+ </div>
102
+ <div class="progress-container" id="uploadProgress">
103
+ <div class="progress-info"><span>Upload</span><span id="uploadInfo"></span></div>
104
+ <div class="progress-bar"><div class="progress-fill" id="uploadFill"></div></div>
105
  </div>
 
106
  <div class="log-container" id="logContainer">
107
  <div class="log-controls">
108
+ <button type="button" class="log-btn" id="clearLogBtn">Clear</button>
109
+ <button type="button" class="log-btn active" id="scrollLockBtn">Auto Scroll</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  </div>
111
  </div>
112
  </div>
113
 
114
+ <div class="video-controls" id="videoControls">
115
+ <h3>Preview & Control</h3>
116
+ <video id="videoPlayer" controls></video>
117
+ <div class="video-button-group">
118
+ <button type="button" class="btn-secondary" id="downloadBtn">Download File</button>
119
+ <button type="button" class="btn-secondary" id="copyLinkBtn">Copy Link</button>
120
+ <button type="button" class="btn-primary" id="cleanupBtn" style="background: var(--error);">Clean Up Server File</button>
121
  </div>
122
  </div>
123
  </div>
124
  </div>
125
 
126
  <script>
127
+ const el = {
128
+ form: document.getElementById('mainForm'), vodUrl: document.getElementById('vodUrl'),
129
+ megaUser: document.getElementById('megaUser'), megaPass: document.getElementById('megaPass'),
130
+ formatSelect: document.getElementById('formatSelect'), qualityBtn: document.getElementById('qualityBtn'),
131
+ startBtn: document.getElementById('startBtn'), statusSection: document.getElementById('statusSection'),
132
+ downloadProgress: document.getElementById('downloadProgress'), downloadInfo: document.getElementById('downloadInfo'),
133
+ downloadFill: document.getElementById('downloadFill'), uploadProgress: document.getElementById('uploadProgress'),
134
+ uploadInfo: document.getElementById('uploadInfo'), uploadFill: document.getElementById('uploadFill'),
135
+ logContainer: document.getElementById('logContainer'), clearLogBtn: document.getElementById('clearLogBtn'),
136
+ scrollLockBtn: document.getElementById('scrollLockBtn'), videoControls: document.getElementById('videoControls'),
137
+ videoPlayer: document.getElementById('videoPlayer'), downloadBtn: document.getElementById('downloadBtn'),
138
+ copyLinkBtn: document.getElementById('copyLinkBtn'), cleanupBtn: document.getElementById('cleanupBtn'),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  };
140
 
141
+ let autoScroll = true; let currentFileId = null;
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ el.qualityBtn.addEventListener('click', fetchQualities);
144
+ el.form.addEventListener('submit', startProcess);
145
+ el.scrollLockBtn.addEventListener('click', () => { autoScroll = !autoScroll; el.scrollLockBtn.classList.toggle('active', autoScroll); });
146
+ el.clearLogBtn.addEventListener('click', () => { const controls = el.logContainer.querySelector('.log-controls'); el.logContainer.innerHTML = ''; el.logContainer.appendChild(controls); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ async function fetchQualities() {
149
+ if (!el.vodUrl.value) { alert('Please enter a VOD URL.'); return; }
150
+ setLoading(el.qualityBtn, true);
151
+ try {
152
+ const response = await fetch('/get_formats', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: el.vodUrl.value }) });
153
+ const data = await response.json();
154
+ if (data.error) throw new Error(data.error);
155
+ el.formatSelect.innerHTML = '';
156
+ data.formats.forEach(f => { const option = document.createElement('option'); option.value = f.id; option.textContent = f.label; el.formatSelect.appendChild(option); });
157
+ el.formatSelect.disabled = false; el.startBtn.disabled = false;
158
+ addLog('Quality options loaded.', 'success');
159
+ } catch (error) { addLog(`Error fetching qualities: ${error.message}`, 'error'); } finally { setLoading(el.qualityBtn, false); }
 
 
 
 
 
 
 
160
  }
161
 
162
+ function startProcess(e) {
163
+ e.preventDefault(); resetUI(); setProcessing(true);
164
+ const payload = { url: el.vodUrl.value, mega_user: el.megaUser.value, mega_pass: el.megaPass.value, format_id: el.formatSelect.value };
165
+ fetch('/process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
166
+ .then(response => {
167
+ const reader = response.body.getReader(); const decoder = new TextDecoder();
168
+ function read() {
169
+ reader.read().then(({ done, value }) => {
170
+ if (done) { handleStreamEnd(); return; }
171
+ decoder.decode(value, { stream: true }).split('\n\n').forEach(line => {
172
+ if (line.startsWith('data:')) handleSSEMessage(line.substring(5));
173
+ });
174
+ read();
175
+ });
176
+ }
177
+ read();
178
+ }).catch(err => { addLog(`Failed to start process: ${err}`, 'error'); setProcessing(false); });
179
  }
180
 
181
+ function handleSSEMessage(jsonString) {
 
 
 
 
 
 
 
 
 
 
182
  try {
183
+ const data = JSON.parse(jsonString);
184
+ switch (data.type) {
185
+ case 'phase':
186
+ addLog(data.text);
187
+ if (data.phase === 'download') el.downloadProgress.style.display = 'block';
188
+ if (data.phase === 'upload') el.uploadProgress.style.display = 'block';
189
+ break;
190
+ case 'progress': updateProgressUI(data.phase, data); break;
191
+ case 'file_ready': currentFileId = data.file_id; setupVideoControls(data.file_id); addLog('File is ready for preview and download.', 'success'); break;
192
+ case 'complete': addLog(data.text, 'success'); break;
193
+ case 'error': addLog(`ERROR: ${data.message}`, 'error'); setProcessing(false); break;
194
+ case 'done': setProcessing(false); break;
195
  }
196
+ } catch (e) { /* Ignore non-json messages */ }
197
+ }
198
 
199
+ function handleStreamEnd() { addLog("Process stream finished."); setProcessing(false); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
+ function updateProgressUI(phase, data) {
202
+ const fill = phase === 'download' ? el.downloadFill : el.uploadFill;
203
+ const info = phase === 'download' ? el.downloadInfo : el.uploadInfo;
204
+ fill.style.width = `${data.percent}%`;
205
+ let infoText = `${data.percent.toFixed(1)}%`;
206
+ if (data.speed) infoText += ` (${data.speed} - ETA: ${data.eta || 'N/A'})`;
207
+ info.textContent = infoText;
208
+ }
209
 
210
+ function setupVideoControls(fileId) {
211
+ el.videoControls.style.display = 'block';
212
+ el.videoPlayer.src = `/video/${fileId}`;
213
+ el.downloadBtn.onclick = () => window.location.href = `/download/${fileId}`;
214
+ el.copyLinkBtn.onclick = () => { navigator.clipboard.writeText(`${window.location.origin}/video/${fileId}`); alert('Link copied!'); };
215
+ el.cleanupBtn.onclick = async () => {
216
+ if (confirm('Are you sure you want to delete the file from the server?')) {
217
+ setLoading(el.cleanupBtn, true);
218
+ await fetch(`/cleanup/${fileId}`, { method: 'POST' });
219
+ addLog(`File ${fileId} cleaned up.`, 'success');
220
+ el.videoControls.style.display = 'none'; currentFileId = null;
221
+ setLoading(el.cleanupBtn, false);
 
222
  }
223
  };
224
+ }
225
 
226
+ function resetUI() {
227
+ el.statusSection.style.display = 'block'; el.videoControls.style.display = 'none';
228
+ const controls = el.logContainer.querySelector('.log-controls'); el.logContainer.innerHTML = ''; el.logContainer.appendChild(controls);
229
+ currentFileId = null;
230
+ ['download', 'upload'].forEach(p => { document.getElementById(`${p}Progress`).style.display = 'none'; document.getElementById(`${p}Fill`).style.width = '0%'; });
231
+ }
 
 
 
 
 
 
 
 
232
 
233
+ function setProcessing(is) { el.startBtn.disabled = is; el.qualityBtn.disabled = is; }
234
+ function setLoading(btn, is) { btn.disabled = is; }
235
  </script>
 
236
  </body>
237
  </html>