eubottura commited on
Commit
f1a8907
·
verified ·
1 Parent(s): 0adc9c4

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +961 -1097
index.html CHANGED
@@ -1,1115 +1,979 @@
1
  <!DOCTYPE html>
2
  <html lang="pt-BR">
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Douyin Video Downloader - Melhorado</title>
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
- <style>
9
- :root {
10
- --primary: #fe2c55;
11
- --secondary: #25f4ee;
12
- --bg-dark: #161823;
13
- --bg-card: #1f1f2e;
14
- --bg-hover: #2a2a3e;
15
- --text-primary: #ffffff;
16
- --text-secondary: #a0a0b0;
17
- --border: #2a2a3e;
18
- --success: #00ff88;
19
- --warning: #ffaa00;
20
- --error: #ff3366;
21
- --gradient: linear-gradient(135deg, #fe2c55 0%, #25f4ee 100%);
22
- }
23
-
24
- * {
25
- margin: 0;
26
- padding: 0;
27
- box-sizing: border-box;
28
- }
29
-
30
- body {
31
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
32
- background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 100%);
33
- color: var(--text-primary);
34
- min-height: 100vh;
35
- display: flex;
36
- flex-direction: column;
37
- }
38
-
39
- .container {
40
- max-width: 1400px;
41
- margin: 0 auto;
42
- padding: 20px;
43
- flex: 1;
44
- width: 100%;
45
- }
46
-
47
- header {
48
- background: rgba(31, 31, 46, 0.8);
49
- backdrop-filter: blur(10px);
50
- border-bottom: 1px solid var(--border);
51
- padding: 20px 0;
52
- position: sticky;
53
- top: 0;
54
- z-index: 100;
55
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
56
- }
57
-
58
- .header-content {
59
- display: flex;
60
- justify-content: space-between;
61
- align-items: center;
62
- max-width: 1400px;
63
- margin: 0 auto;
64
- padding: 0 20px;
65
- }
66
-
67
- .logo {
68
- display: flex;
69
- align-items: center;
70
- gap: 15px;
71
- }
72
-
73
- .logo-icon {
74
- width: 50px;
75
- height: 50px;
76
- background: var(--gradient);
77
- border-radius: 12px;
78
- display: flex;
79
- align-items: center;
80
- justify-content: center;
81
- font-size: 24px;
82
- animation: pulse 2s infinite;
83
- }
84
-
85
- @keyframes pulse {
86
- 0%, 100% { transform: scale(1); }
87
- 50% { transform: scale(1.05); }
88
- }
89
-
90
- h1 {
91
- font-size: 1.8rem;
92
- font-weight: 700;
93
- background: var(--gradient);
94
- -webkit-background-clip: text;
95
- -webkit-text-fill-color: transparent;
96
- background-clip: text;
97
- }
98
-
99
- .built-with {
100
- color: var(--text-secondary);
101
- text-decoration: none;
102
- font-size: 0.9rem;
103
- transition: color 0.3s;
104
- }
105
-
106
- .built-with:hover {
107
- color: var(--secondary);
108
- }
109
-
110
- .main-content {
111
- display: grid;
112
- grid-template-columns: 1fr 1fr;
113
- gap: 30px;
114
- margin-top: 30px;
115
- }
116
-
117
- .card {
118
- background: rgba(31, 31, 46, 0.6);
119
- backdrop-filter: blur(10px);
120
- border: 1px solid var(--border);
121
- border-radius: 20px;
122
- padding: 30px;
123
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
124
- transition: transform 0.3s, box-shadow 0.3s;
125
- }
126
-
127
- .card:hover {
128
- transform: translateY(-5px);
129
- box-shadow: 0 15px 50px rgba(254, 44, 85, 0.2);
130
- }
131
-
132
- .card-header {
133
- display: flex;
134
- align-items: center;
135
- gap: 15px;
136
- margin-bottom: 25px;
137
- }
138
-
139
- .card-icon {
140
- width: 45px;
141
- height: 45px;
142
- background: var(--gradient);
143
- border-radius: 10px;
144
- display: flex;
145
- align-items: center;
146
- justify-content: center;
147
- font-size: 20px;
148
- }
149
-
150
- .card-title {
151
- font-size: 1.3rem;
152
- font-weight: 600;
153
- }
154
-
155
- .input-section {
156
- margin-bottom: 25px;
157
- }
158
-
159
- .input-label {
160
- display: block;
161
- margin-bottom: 10px;
162
- color: var(--text-secondary);
163
- font-weight: 500;
164
- }
165
-
166
- .links-textarea {
167
- width: 100%;
168
- min-height: 200px;
169
- padding: 15px;
170
- background: var(--bg-dark);
171
- border: 2px solid var(--border);
172
- border-radius: 12px;
173
- color: var(--text-primary);
174
- font-size: 14px;
175
- resize: vertical;
176
- transition: border-color 0.3s;
177
- }
178
-
179
- .links-textarea:focus {
180
- outline: none;
181
- border-color: var(--primary);
182
- }
183
-
184
- .url-chips {
185
- display: flex;
186
- flex-wrap: wrap;
187
- gap: 8px;
188
- margin-top: 10px;
189
- }
190
-
191
- .url-chip {
192
- background: var(--bg-hover);
193
- padding: 6px 12px;
194
- border-radius: 20px;
195
- font-size: 12px;
196
- color: var(--text-secondary);
197
- display: flex;
198
- align-items: center;
199
- gap: 8px;
200
- animation: fadeIn 0.3s;
201
- }
202
-
203
- @keyframes fadeIn {
204
- from { opacity: 0; transform: scale(0.8); }
205
- to { opacity: 1; transform: scale(1); }
206
- }
207
-
208
- .url-chip .remove {
209
- cursor: pointer;
210
- color: var(--error);
211
- transition: transform 0.2s;
212
- }
213
-
214
- .url-chip .remove:hover {
215
- transform: scale(1.2);
216
- }
217
-
218
- .action-buttons {
219
- display: flex;
220
- gap: 15px;
221
- margin-top: 20px;
222
- }
223
-
224
- .btn {
225
- flex: 1;
226
- padding: 14px 24px;
227
- border: none;
228
- border-radius: 10px;
229
- font-size: 1rem;
230
- font-weight: 600;
231
- cursor: pointer;
232
- transition: all 0.3s;
233
- display: flex;
234
- align-items: center;
235
- justify-content: center;
236
- gap: 10px;
237
- }
238
-
239
- .btn-primary {
240
- background: var(--gradient);
241
- color: white;
242
- }
243
-
244
- .btn-primary:hover:not(:disabled) {
245
- transform: translateY(-3px);
246
- box-shadow: 0 5px 20px rgba(254, 44, 85, 0.4);
247
- }
248
-
249
- .btn-secondary {
250
- background: var(--bg-hover);
251
- color: var(--text-primary);
252
- }
253
-
254
- .btn-secondary:hover:not(:disabled) {
255
- background: #3a3a4e;
256
- }
257
-
258
- .btn:disabled {
259
- opacity: 0.5;
260
- cursor: not-allowed;
261
- }
262
-
263
- .progress-section {
264
- margin-top: 30px;
265
- display: none;
266
- }
267
-
268
- .progress-header {
269
- display: flex;
270
- justify-content: space-between;
271
- align-items: center;
272
- margin-bottom: 15px;
273
- }
274
-
275
- .progress-title {
276
- font-size: 1.1rem;
277
- color: var(--text-primary);
278
- }
279
-
280
- .progress-stats {
281
- display: flex;
282
- gap: 20px;
283
- font-size: 0.9rem;
284
- }
285
-
286
- .stat {
287
- display: flex;
288
- align-items: center;
289
- gap: 5px;
290
- }
291
-
292
- .stat.success { color: var(--success); }
293
- .stat.error { color: var(--error); }
294
- .stat.pending { color: var(--warning); }
295
-
296
- .progress-bar {
297
- width: 100%;
298
- height: 8px;
299
- background: var(--bg-dark);
300
- border-radius: 10px;
301
- overflow: hidden;
302
- margin-bottom: 20px;
303
- }
304
-
305
- .progress-fill {
306
- height: 100%;
307
- background: var(--gradient);
308
- border-radius: 10px;
309
- transition: width 0.3s ease;
310
- width: 0%;
311
- }
312
-
313
- .downloads-list {
314
- max-height: 400px;
315
- overflow-y: auto;
316
- margin-top: 20px;
317
- }
318
-
319
- .download-item {
320
- background: var(--bg-dark);
321
- border: 1px solid var(--border);
322
- border-radius: 12px;
323
- padding: 15px;
324
- margin-bottom: 15px;
325
- display: flex;
326
- align-items: center;
327
- gap: 15px;
328
- transition: all 0.3s;
329
- }
330
-
331
- .download-item:hover {
332
- border-color: var(--primary);
333
- transform: translateX(5px);
334
- }
335
-
336
- .download-status {
337
- width: 40px;
338
- height: 40px;
339
- border-radius: 50%;
340
- display: flex;
341
- align-items: center;
342
- justify-content: center;
343
- flex-shrink: 0;
344
- }
345
-
346
- .status-pending {
347
- background: rgba(255, 170, 0, 0.2);
348
- color: var(--warning);
349
- }
350
-
351
- .status-processing {
352
- background: rgba(37, 244, 238, 0.2);
353
- color: var(--secondary);
354
- animation: spin 1s linear infinite;
355
- }
356
-
357
- @keyframes spin {
358
- from { transform: rotate(0deg); }
359
- to { transform: rotate(360deg); }
360
- }
361
-
362
- .status-success {
363
- background: rgba(0, 255, 136, 0.2);
364
- color: var(--success);
365
- }
366
-
367
- .status-error {
368
- background: rgba(255, 51, 102, 0.2);
369
- color: var(--error);
370
- }
371
-
372
- .download-info {
373
- flex: 1;
374
- }
375
-
376
- .download-url {
377
- font-size: 0.9rem;
378
- color: var(--text-secondary);
379
- margin-bottom: 5px;
380
- word-break: break-all;
381
- }
382
-
383
- .download-details {
384
- display: flex;
385
- gap: 15px;
386
- font-size: 0.85rem;
387
- color: var(--text-secondary);
388
- }
389
-
390
- .download-actions {
391
- display: flex;
392
- gap: 10px;
393
- }
394
-
395
- .action-btn {
396
- width: 35px;
397
- height: 35px;
398
- border-radius: 8px;
399
- border: none;
400
- background: var(--bg-hover);
401
- color: var(--text-secondary);
402
- cursor: pointer;
403
- display: flex;
404
- align-items: center;
405
- justify-content: center;
406
- transition: all 0.3s;
407
- }
408
-
409
- .action-btn:hover {
410
- background: var(--primary);
411
- color: white;
412
- transform: scale(1.1);
413
- }
414
-
415
- .settings-section {
416
- margin-top: 20px;
417
- }
418
-
419
- .setting-item {
420
- display: flex;
421
- justify-content: space-between;
422
- align-items: center;
423
- padding: 15px;
424
- background: var(--bg-dark);
425
- border-radius: 10px;
426
- margin-bottom: 10px;
427
- }
428
-
429
- .setting-label {
430
- color: var(--text-secondary);
431
- font-size: 0.9rem;
432
- }
433
-
434
- .toggle-switch {
435
- position: relative;
436
- width: 50px;
437
- height: 26px;
438
- background: var(--bg-hover);
439
- border-radius: 13px;
440
- cursor: pointer;
441
- transition: background 0.3s;
442
- }
443
-
444
- .toggle-switch.active {
445
- background: var(--gradient);
446
- }
447
-
448
- .toggle-switch::after {
449
- content: '';
450
- position: absolute;
451
- width: 22px;
452
- height: 22px;
453
- background: white;
454
- border-radius: 50%;
455
- top: 2px;
456
- left: 2px;
457
- transition: transform 0.3s;
458
- }
459
-
460
- .toggle-switch.active::after {
461
- transform: translateX(24px);
462
- }
463
-
464
- .notification {
465
- position: fixed;
466
- bottom: 20px;
467
- right: 20px;
468
- background: var(--bg-card);
469
- border: 1px solid var(--border);
470
- border-radius: 12px;
471
- padding: 15px 20px;
472
- display: flex;
473
- align-items: center;
474
- gap: 15px;
475
- box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
476
- transform: translateX(400px);
477
- transition: transform 0.3s;
478
- z-index: 1000;
479
- }
480
-
481
- .notification.show {
482
- transform: translateX(0);
483
- }
484
-
485
- .notification-icon {
486
- width: 30px;
487
- height: 30px;
488
- border-radius: 50%;
489
- display: flex;
490
- align-items: center;
491
- justify-content: center;
492
- }
493
-
494
- .notification.success .notification-icon {
495
- background: rgba(0, 255, 136, 0.2);
496
- color: var(--success);
497
- }
498
-
499
- .notification.error .notification-icon {
500
- background: rgba(255, 51, 102, 0.2);
501
- color: var(--error);
502
- }
503
-
504
- .api-selector {
505
- margin-bottom: 20px;
506
- padding: 15px;
507
- background: var(--bg-dark);
508
- border-radius: 10px;
509
- }
510
-
511
- .api-selector label {
512
- display: block;
513
- margin-bottom: 10px;
514
- color: var(--text-secondary);
515
- font-size: 0.9rem;
516
- }
517
-
518
- .api-selector select {
519
- width: 100%;
520
- padding: 10px;
521
- background: var(--bg-hover);
522
- border: 1px solid var(--border);
523
- border-radius: 8px;
524
- color: var(--text-primary);
525
- cursor: pointer;
526
- }
527
-
528
- @media (max-width: 968px) {
529
- .main-content {
530
- grid-template-columns: 1fr;
531
- }
532
- }
533
-
534
- .empty-state {
535
- text-align: center;
536
- padding: 40px;
537
- color: var(--text-secondary);
538
- }
539
-
540
- .empty-state i {
541
- font-size: 48px;
542
- margin-bottom: 15px;
543
- opacity: 0.5;
544
- }
545
-
546
- .debug-info {
547
- margin-top: 10px;
548
- padding: 10px;
549
- background: rgba(255, 0, 0, 0.1);
550
- border-radius: 8px;
551
- font-size: 0.8rem;
552
- color: var(--error);
553
- }
554
- </style>
555
- </head>
556
- <body>
557
- <header>
558
- <div class="header-content">
559
- <div class="logo">
560
- <div class="logo-icon">
561
- <i class="fas fa-download"></i>
562
- </div>
563
- <h1>Douyin Video Downloader</h1>
564
- </div>
565
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with">
566
- Built with anycoder
567
- </a>
568
- </div>
569
- </header>
570
-
571
- <div class="container">
572
- <div class="main-content">
573
- <div class="card">
574
- <div class="card-header">
575
- <div class="card-icon">
576
- <i class="fas fa-link"></i>
577
- </div>
578
- <div class="card-title">Adicionar Links do Douyin</div>
579
- </div>
580
 
581
- <div class="api-selector">
582
- <label for="apiSelect">Selecionar API:</label>
583
- <select id="apiSelect" onchange="changeApi()">
584
- <option value="tikmate">TikMate Online</option>
585
- <option value="snaptik">SnapTik</option>
586
- <option value="musicallydown">MusicallyDown</option>
587
- <option value="direct">Método Direto (Experimental)</option>
588
- </select>
589
- </div>
590
 
591
- <div class="input-section">
592
- <label class="input-label">Cole os links dos vídeos (um por linha):</label>
593
- <textarea
594
- id="linksInput"
595
- class="links-textarea"
596
- placeholder="https://www.douyin.com/video/7123456789012345678&#10;https://v.douyin.com/ABC123&#10;https://www.douyin.com/video/7123456789012345679"
597
- ></textarea>
598
- <div id="urlChips" class="url-chips"></div>
599
- </div>
600
 
601
- <div class="action-buttons">
602
- <button class="btn btn-primary" onclick="startDownloads()" id="startBtn">
603
- <i class="fas fa-play"></i>
604
- Iniciar Downloads
605
- </button>
606
- <button class="btn btn-secondary" onclick="clearLinks()">
607
- <i class="fas fa-trash"></i>
608
- Limpar
609
- </button>
610
- </div>
611
 
612
- <div class="progress-section" id="progressSection">
613
- <div class="progress-header">
614
- <div class="progress-title">Progresso dos Downloads</div>
615
- <div class="progress-stats">
616
- <div class="stat success">
617
- <i class="fas fa-check-circle"></i>
618
- <span id="successCount">0</span>
619
- </div>
620
- <div class="stat error">
621
- <i class="fas fa-times-circle"></i>
622
- <span id="errorCount">0</span>
623
- </div>
624
- <div class="stat pending">
625
- <i class="fas fa-clock"></i>
626
- <span id="pendingCount">0</span>
627
- </div>
628
- </div>
629
- </div>
630
- <div class="progress-bar">
631
- <div class="progress-fill" id="progressFill"></div>
632
- </div>
633
- <div class="downloads-list" id="downloadsList"></div>
634
- </div>
635
- </div>
636
-
637
- <div class="card">
638
- <div class="card-header">
639
- <div class="card-icon">
640
- <i class="fas fa-cog"></i>
641
- </div>
642
- <div class="card-title">Configurações</div>
643
- </div>
644
 
645
- <div class="settings-section">
646
- <div class="setting-item">
647
- <span class="setting-label">Baixar automaticamente após extração</span>
648
- <div class="toggle-switch active" onclick="toggleSetting(this, 'autoDownload')"></div>
649
- </div>
650
- <div class="setting-item">
651
- <span class="setting-label">Selecionar máxima qualidade</span>
652
- <div class="toggle-switch active" onclick="toggleSetting(this, 'maxQuality')"></div>
653
- </div>
654
- <div class="setting-item">
655
- <span class="setting-label">Mostrar notificações</span>
656
- <div class="toggle-switch active" onclick="toggleSetting(this, 'notifications')"></div>
657
- </div>
658
- <div class="setting-item">
659
- <span class="setting-label">Processamento simultâneo</span>
660
- <div class="toggle-switch" onclick="toggleSetting(this, 'parallel')"></div>
661
- </div>
662
- <div class="setting-item">
663
- <span class="setting-label">Modo Debug (mostrar erros)</span>
664
- <div class="toggle-switch" onclick="toggleSetting(this, 'debug')"></div>
665
- </div>
666
- </div>
667
 
668
- <div style="margin-top: 30px; padding: 20px; background: rgba(254, 44, 85, 0.1); border-radius: 12px;">
669
- <h3 style="color: var(--primary); margin-bottom: 10px;">
670
- <i class="fas fa-info-circle"></i> Como Funciona
671
- </h3>
672
- <ol style="color: var(--text-secondary); margin-left: 20px; line-height: 1.8;">
673
- <li>Selecione uma API funcional no menu suspenso</li>
674
- <li>Cole os links dos vídeos do Douyin na área de texto</li>
675
- <li>Clique em "Iniciar Downloads" para começar o processamento</li>
676
- <li>O sistema tentará extrair os links usando múltiplos métodos</li>
677
- <li>Se falhar, tente outra API da lista</li>
678
- <li>Use o modo Debug para ver detalhes dos erros</li>
679
- </ol>
680
- </div>
681
 
682
- <div style="margin-top: 20px;">
683
- <h3 style="color: var(--text-primary); margin-bottom: 15px;">
684
- <i class="fas fa-chart-line"></i> Estatísticas
685
- </h3>
686
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
687
- <div style="background: var(--bg-dark); padding: 15px; border-radius: 10px;">
688
- <div style="color: var(--text-secondary); font-size: 0.85rem;">Total Processado</div>
689
- <div style="font-size: 1.5rem; font-weight: bold; color: var(--primary);" id="totalProcessed">0</div>
690
- </div>
691
- <div style="background: var(--bg-dark); padding: 15px; border-radius: 10px;">
692
- <div style="color: var(--text-secondary); font-size: 0.85rem;">Taxa de Sucesso</div>
693
- <div style="font-size: 1.5rem; font-weight: bold; color: var(--success);" id="successRate">0%</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  </div>
695
- </div>
 
 
696
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
 
698
- <div style="margin-top: 20px; padding: 15px; background: rgba(255, 170, 0, 0.1); border-radius: 12px;">
699
- <p style="color: var(--warning); font-size: 0.9rem;">
700
- <i class="fas fa-exclamation-triangle"></i>
701
- <strong>Atenção:</strong> APIs de terceiros podem ficar indisponíveis.
702
- Se uma falhar, experimente outra opção.
703
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  </div>
705
- </div>
706
  </div>
707
- </div>
708
 
709
- <div class="notification" id="notification">
710
- <div class="notification-icon">
711
- <i class="fas fa-check"></i>
712
- </div>
713
- <div class="notification-content">
714
- <div id="notificationText" style="font-weight: 600;"></div>
715
- </div>
716
- </div>
717
-
718
- <script>
719
- let settings = {
720
- autoDownload: true,
721
- maxQuality: true,
722
- notifications: true,
723
- parallel: false,
724
- debug: false
725
- };
726
-
727
- let downloadQueue = [];
728
- let processedCount = 0;
729
- let successCount = 0;
730
- let errorCount = 0;
731
- let currentApi = 'tikmate';
732
-
733
- function toggleSetting(element, setting) {
734
- element.classList.toggle('active');
735
- settings[setting] = element.classList.contains('active');
736
- }
737
-
738
- function changeApi() {
739
- currentApi = document.getElementById('apiSelect').value;
740
- showNotification(`API alterada para: ${currentApi}`, 'success');
741
- }
742
-
743
- function parseLinks() {
744
- const input = document.getElementById('linksInput').value;
745
- const urls = input.split('\n')
746
- .map(url => url.trim())
747
- .filter(url => url && (url.includes('douyin.com') || url.includes('v.douyin.com')));
748
-
749
- const uniqueUrls = [...new Set(urls)];
750
- updateUrlChips(uniqueUrls);
751
- return uniqueUrls;
752
- }
753
-
754
- function updateUrlChips(urls) {
755
- const chipsContainer = document.getElementById('urlChips');
756
- chipsContainer.innerHTML = '';
757
-
758
- urls.forEach((url, index) => {
759
- const chip = document.createElement('div');
760
- chip.className = 'url-chip';
761
- chip.innerHTML = `
762
- <i class="fas fa-link"></i>
763
- <span>${url.substring(0, 40)}${url.length > 40 ? '...' : ''}</span>
764
- <i class="fas fa-times remove" onclick="removeUrl(${index})"></i>
765
- `;
766
- chipsContainer.appendChild(chip);
767
- });
768
- }
769
-
770
- function removeUrl(index) {
771
- const input = document.getElementById('linksInput');
772
- const urls = input.value.split('\n');
773
- urls.splice(index, 1);
774
- input.value = urls.join('\n');
775
- parseLinks();
776
- }
777
-
778
- function clearLinks() {
779
- document.getElementById('linksInput').value = '';
780
- document.getElementById('urlChips').innerHTML = '';
781
- document.getElementById('progressSection').style.display = 'none';
782
- resetStats();
783
- }
784
-
785
- function resetStats() {
786
- processedCount = 0;
787
- successCount = 0;
788
- errorCount = 0;
789
- downloadQueue = [];
790
- updateProgressDisplay();
791
- }
792
-
793
- // Métodos de extração alternativos
794
- async function extractWithTikmate(url) {
795
- try {
796
- const response = await fetch(`https://tikmate.online/download?url=${encodeURIComponent(url)}`);
797
- const html = await response.text();
798
-
799
- // Parser simples para extrair o link
800
- const parser = new DOMParser();
801
- const doc = parser.parseFromString(html, 'text/html');
802
- const downloadLink = doc.querySelector('a[href*=".mp4"]');
803
-
804
- if (downloadLink) {
805
- return {
806
- success: true,
807
- downloadUrl: downloadLink.href,
808
- quality: 'HD',
809
- size: 'Unknown',
810
- title: 'Douyin Video'
811
- };
812
- }
813
- throw new Error('No download link found');
814
- } catch (error) {
815
- return { success: false, error: error.message };
816
- }
817
- }
818
-
819
- async function extractWithSnaptik(url) {
820
- try {
821
- const response = await fetch(`https://snaptik.app/abc?url=${encodeURIComponent(url)}`);
822
- const data = await response.json();
823
-
824
- if (data.status === 'success' && data.data) {
825
- return {
826
- success: true,
827
- downloadUrl: data.data,
828
- quality: 'HD',
829
- size: 'Unknown',
830
- title: 'Douyin Video'
831
- };
832
- }
833
- throw new Error(data.message || 'Failed to extract');
834
- } catch (error) {
835
- return { success: false, error: error.message };
836
- }
837
- }
838
-
839
- async function extractWithMusicallydown(url) {
840
- try {
841
- const response = await fetch(`https://musicallydown.com/download?url=${encodeURIComponent(url)}`);
842
- const html = await response.text();
843
-
844
- const parser = new DOMParser();
845
- const doc = parser.parseFromString(html, 'text/html');
846
- const downloadLink = doc.querySelector('a.download-link');
847
-
848
- if (downloadLink) {
849
- return {
850
- success: true,
851
- downloadUrl: downloadLink.href,
852
- quality: 'HD',
853
- size: 'Unknown',
854
- title: 'Douyin Video'
855
- };
856
- }
857
- throw new Error('No download link found');
858
- } catch (error) {
859
- return { success: false, error: error.message };
860
- }
861
- }
862
-
863
- async function extractDirect(url) {
864
- try {
865
- // Tentativa direta com headers customizados
866
- const response = await fetch(url, {
867
- headers: {
868
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
869
- }
870
- });
871
-
872
- const html = await response.text();
873
-
874
- // Procurar por padrões de vídeo no HTML
875
- const videoMatch = html.match(/"play_addr":\s*{\s*"url_list":\s*\["([^"]+)"\]/);
876
-
877
- if (videoMatch && videoMatch[1]) {
878
- return {
879
- success: true,
880
- downloadUrl: videoMatch[1].replace(/\\\\u002F/g, '/'),
881
- quality: 'HD',
882
- size: 'Unknown',
883
- title: 'Douyin Video'
884
- };
885
- }
886
-
887
- throw new Error('Direct extraction failed');
888
- } catch (error) {
889
- return { success: false, error: error.message };
890
- }
891
- }
892
-
893
- async function extractVideoUrl(url) {
894
- let result = { success: false, error: 'All methods failed' };
895
-
896
- // Tentar diferentes métodos baseado na API selecionada
897
- switch(currentApi) {
898
- case 'tikmate':
899
- result = await extractWithTikmate(url);
900
- break;
901
- case 'snaptik':
902
- result = await extractWithSnaptik(url);
903
- break;
904
- case 'musicallydown':
905
- result = await extractWithMusicallydown(url);
906
- break;
907
- case 'direct':
908
- result = await extractDirect(url);
909
- break;
910
- }
911
-
912
- // Se falhar, tentar fallback com outras APIs
913
- if (!result.success) {
914
- if (settings.debug) {
915
- console.log(`${currentApi} failed, trying fallback...`);
916
- }
917
-
918
- const fallbackMethods = [extractWithTikmate, extractWithSnaptik, extractWithMusicallydown, extractDirect];
919
-
920
- for (const method of fallbackMethods) {
921
- if (!result.success) {
922
- result = await method(url);
923
- if (result.success && settings.debug) {
924
- console.log('Success with fallback method');
925
- }
926
- }
927
- }
928
- }
929
-
930
- return result;
931
- }
932
-
933
- function createDownloadItem(url, index) {
934
- const item = document.createElement('div');
935
- item.className = 'download-item';
936
- item.id = `download-${index}`;
937
- item.innerHTML = `
938
- <div class="download-status status-pending">
939
- <i class="fas fa-clock"></i>
940
- </div>
941
- <div class="download-info">
942
- <div class="download-url">${url}</div>
943
- <div class="download-details">
944
- <span id="quality-${index}">Aguardando...</span>
945
- <span id="size-${index}">--</span>
946
- </div>
947
- <div id="debug-${index}" class="debug-info" style="display: none;"></div>
948
  </div>
949
- <div class="download-actions">
950
- <button class="action-btn" onclick="copyLink('${index}')" title="Copiar Link">
951
- <i class="fas fa-copy"></i>
952
- </button>
953
- <button class="action-btn" onclick="downloadVideo('${index}')" title="Baixar">
954
- <i class="fas fa-download"></i>
955
- </button>
956
  </div>
957
- `;
958
- return item;
959
- }
960
-
961
- function updateDownloadStatus(index, status, data = {}) {
962
- const item = document.getElementById(`download-${index}`);
963
- if (!item) return;
964
-
965
- const statusDiv = item.querySelector('.download-status');
966
- const qualitySpan = document.getElementById(`quality-${index}`);
967
- const sizeSpan = document.getElementById(`size-${index}`);
968
- const debugDiv = document.getElementById(`debug-${index}`);
969
-
970
- statusDiv.className = `download-status status-${status}`;
971
-
972
- switch(status) {
973
- case 'processing':
974
- statusDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
975
- break;
976
- case 'success':
977
- statusDiv.innerHTML = '<i class="fas fa-check"></i>';
978
- qualitySpan.textContent = data.quality || 'HD';
979
- sizeSpan.textContent = data.size || '--';
980
- if (settings.autoDownload) {
981
- downloadVideo(index, data.downloadUrl);
982
- }
983
- break;
984
- case 'error':
985
- statusDiv.innerHTML = '<i class="fas fa-times"></i>';
986
- qualitySpan.textContent = 'Erro';
987
- sizeSpan.textContent = 'Falha na extração';
988
- if (settings.debug && debugDiv) {
989
- debugDiv.style.display = 'block';
990
- debugDiv.textContent = `Erro: ${data.error || 'Unknown error'}`;
991
- }
992
- break;
993
- }
994
- }
995
-
996
- async function processDownload(url, index) {
997
- updateDownloadStatus(index, 'processing');
998
-
999
- const result = await extractVideoUrl(url);
1000
-
1001
- if (result.success) {
1002
- downloadQueue[index] = result;
1003
- updateDownloadStatus(index, 'success', result);
1004
- successCount++;
1005
-
1006
- if (settings.notifications) {
1007
- showNotification('Download extraído com sucesso!', 'success');
1008
- }
1009
- } else {
1010
- updateDownloadStatus(index, 'error', result);
1011
- errorCount++;
1012
-
1013
- if (settings.notifications) {
1014
- showNotification(`Falha: ${result.error}`, 'error');
1015
- }
1016
- }
1017
-
1018
- processedCount++;
1019
- updateProgressDisplay();
1020
- }
1021
-
1022
- async function startDownloads() {
1023
- const urls = parseLinks();
1024
-
1025
- if (urls.length === 0) {
1026
- showNotification('Por favor, adicione pelo menos um link', 'error');
1027
- return;
1028
- }
1029
-
1030
- // Resetar contadores
1031
- processedCount = 0;
1032
- successCount = 0;
1033
- errorCount = 0;
1034
- downloadQueue = new Array(urls.length);
1035
-
1036
- // Mostrar seção de progresso
1037
- document.getElementById('progressSection').style.display = 'block';
1038
- document.getElementById('downloadsList').innerHTML = '';
1039
-
1040
- // Criar itens de download
1041
- urls.forEach((url, index) => {
1042
- const item = createDownloadItem(url, index);
1043
- document.getElementById('downloadsList').appendChild(item);
1044
- });
1045
-
1046
- // Desabilitar botão
1047
- document.getElementById('startBtn').disabled = true;
1048
-
1049
- // Processar downloads
1050
- if (settings.parallel) {
1051
- await Promise.all(urls.map((url, index) => processDownload(url, index)));
1052
- } else {
1053
- for (let i = 0; i < urls.length; i++) {
1054
- await processDownload(urls[i], i);
1055
- }
1056
- }
1057
-
1058
- // Reabilitar botão
1059
- document.getElementById('startBtn').disabled = false;
1060
-
1061
- // Notificação final
1062
- if (settings.notifications) {
1063
- const message = `Concluído! ${successCount} de ${urls.length} vídeos extraídos.`;
1064
- showNotification(message, successCount > 0 ? 'success' : 'error');
1065
- }
1066
- }
1067
-
1068
- function updateProgressDisplay() {
1069
- const total = downloadQueue.length;
1070
- const progress = total > 0 ? (processedCount / total) * 100 : 0;
1071
-
1072
- document.getElementById('progressFill').style.width = `${progress}%`;
1073
- document.getElementById('successCount').textContent = successCount;
1074
- document.getElementById('errorCount').textContent = errorCount;
1075
- document.getElementById('pendingCount').textContent = total - processedCount;
1076
- document.getElementById('totalProcessed').textContent = processedCount;
1077
-
1078
- const successRate = total > 0 ? Math.round((successCount / total) * 100) : 0;
1079
- document.getElementById('successRate').textContent = `${successRate}%`;
1080
- }
1081
-
1082
- function copyLink(index) {
1083
- const data = downloadQueue[index];
1084
- if (data && data.downloadUrl) {
1085
- navigator.clipboard.writeText(data.downloadUrl);
1086
- showNotification('Link copiado!', 'success');
1087
- }
1088
- }
1089
-
1090
- function downloadVideo(index, directUrl = null) {
1091
- const data = downloadQueue[index];
1092
- const url = directUrl || (data ? data.downloadUrl : null);
1093
-
1094
- if (url) {
1095
- const a = document.createElement('a');
1096
- a.href = url;
1097
- a.download = `douyin_video_${index}.mp4`;
1098
- a.target = '_blank';
1099
- document.body.appendChild(a);
1100
- a.click();
1101
- document.body.removeChild(a);
1102
-
1103
- if (!directUrl && settings.notifications) {
1104
- showNotification('Download iniciado!', 'success');
1105
- }
1106
- }
1107
- }
1108
-
1109
- function showNotification(message, type = 'success') {
1110
- const notification = document.getElementById('notification');
1111
- const icon = notification.querySelector('.notification-icon i');
1112
- const text = document.getElementById('notificationText');
1113
-
1114
- notification.className = `notification ${type}`;
1115
- icon.className = type === 'success' ?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!DOCTYPE html>
2
  <html lang="pt-BR">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Douyin Video Downloader - High Quality Extractor</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <style>
9
+ :root {
10
+ --primary: #fe2c55;
11
+ --secondary: #25f4ee;
12
+ --bg-dark: #161823;
13
+ --bg-card: #1f1f2e;
14
+ --bg-hover: #2a2a3e;
15
+ --text-primary: #ffffff;
16
+ --text-secondary: #a0a0b0;
17
+ --border: #2a2a3e;
18
+ --success: #00ff88;
19
+ --warning: #ffaa00;
20
+ --error: #ff3366;
21
+ --gradient: linear-gradient(135deg, #fe2c55 0%, #25f4ee 100%);
22
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ * {
25
+ margin: 0;
26
+ padding: 0;
27
+ box-sizing: border-box;
28
+ }
 
 
 
 
29
 
30
+ body {
31
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
32
+ background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 100%);
33
+ color: var(--text-primary);
34
+ min-height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ }
 
38
 
39
+ .container {
40
+ max-width: 1400px;
41
+ margin: 0 auto;
42
+ padding: 20px;
43
+ flex: 1;
44
+ width: 100%;
45
+ }
 
 
 
46
 
47
+ header {
48
+ background: rgba(31, 31, 46, 0.8);
49
+ backdrop-filter: blur(10px);
50
+ border-bottom: 1px solid var(--border);
51
+ padding: 20px 0;
52
+ position: sticky;
53
+ top: 0;
54
+ z-index: 100;
55
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
56
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ .header-content {
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ max-width: 1400px;
63
+ margin: 0 auto;
64
+ padding: 0 20px;
65
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ .logo {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 15px;
71
+ }
 
 
 
 
 
 
 
 
72
 
73
+ .logo-icon {
74
+ width: 50px;
75
+ height: 50px;
76
+ background: var(--gradient);
77
+ border-radius: 12px;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ font-size: 24px;
82
+ animation: pulse 2s infinite;
83
+ }
84
+
85
+ @keyframes pulse {
86
+ 0%, 100% { transform: scale(1); }
87
+ 50% { transform: scale(1.05); }
88
+ }
89
+
90
+ h1 {
91
+ font-size: 1.8rem;
92
+ font-weight: 700;
93
+ background: var(--gradient);
94
+ -webkit-background-clip: text;
95
+ -webkit-text-fill-color: transparent;
96
+ background-clip: text;
97
+ }
98
+
99
+ .built-with {
100
+ color: var(--text-secondary);
101
+ text-decoration: none;
102
+ font-size: 0.9rem;
103
+ transition: color 0.3s;
104
+ }
105
+
106
+ .built-with:hover {
107
+ color: var(--secondary);
108
+ }
109
+
110
+ .main-content {
111
+ display: grid;
112
+ grid-template-columns: 1fr 1fr;
113
+ gap: 30px;
114
+ margin-top: 30px;
115
+ }
116
+
117
+ .card {
118
+ background: rgba(31, 31, 46, 0.6);
119
+ backdrop-filter: blur(10px);
120
+ border: 1px solid var(--border);
121
+ border-radius: 20px;
122
+ padding: 30px;
123
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
124
+ transition: transform 0.3s, box-shadow 0.3s;
125
+ }
126
+
127
+ .card:hover {
128
+ transform: translateY(-5px);
129
+ box-shadow: 0 15px 50px rgba(254, 44, 85, 0.2);
130
+ }
131
+
132
+ .card-header {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 15px;
136
+ margin-bottom: 25px;
137
+ }
138
+
139
+ .card-icon {
140
+ width: 45px;
141
+ height: 45px;
142
+ background: var(--gradient);
143
+ border-radius: 10px;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ font-size: 20px;
148
+ }
149
+
150
+ .card-title {
151
+ font-size: 1.3rem;
152
+ font-weight: 600;
153
+ }
154
+
155
+ .input-section {
156
+ margin-bottom: 25px;
157
+ }
158
+
159
+ .input-label {
160
+ display: block;
161
+ margin-bottom: 10px;
162
+ color: var(--text-secondary);
163
+ font-weight: 500;
164
+ }
165
+
166
+ .links-textarea {
167
+ width: 100%;
168
+ min-height: 200px;
169
+ padding: 15px;
170
+ background: var(--bg-dark);
171
+ border: 2px solid var(--border);
172
+ border-radius: 12px;
173
+ color: var(--text-primary);
174
+ font-size: 14px;
175
+ resize: vertical;
176
+ transition: border-color 0.3s;
177
+ }
178
+
179
+ .links-textarea:focus {
180
+ outline: none;
181
+ border-color: var(--primary);
182
+ }
183
+
184
+ .url-chips {
185
+ display: flex;
186
+ flex-wrap: wrap;
187
+ gap: 8px;
188
+ margin-top: 10px;
189
+ }
190
+
191
+ .url-chip {
192
+ background: var(--bg-hover);
193
+ padding: 6px 12px;
194
+ border-radius: 20px;
195
+ font-size: 12px;
196
+ color: var(--text-secondary);
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 8px;
200
+ animation: fadeIn 0.3s;
201
+ }
202
+
203
+ @keyframes fadeIn {
204
+ from { opacity: 0; transform: scale(0.8); }
205
+ to { opacity: 1; transform: scale(1); }
206
+ }
207
+
208
+ .url-chip .remove {
209
+ cursor: pointer;
210
+ color: var(--error);
211
+ transition: transform 0.2s;
212
+ }
213
+
214
+ .url-chip .remove:hover {
215
+ transform: scale(1.2);
216
+ }
217
+
218
+ .action-buttons {
219
+ display: flex;
220
+ gap: 15px;
221
+ margin-top: 20px;
222
+ }
223
+
224
+ .btn {
225
+ flex: 1;
226
+ padding: 14px 24px;
227
+ border: none;
228
+ border-radius: 10px;
229
+ font-size: 1rem;
230
+ font-weight: 600;
231
+ cursor: pointer;
232
+ transition: all 0.3s;
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ gap: 10px;
237
+ }
238
+
239
+ .btn-primary {
240
+ background: var(--gradient);
241
+ color: white;
242
+ }
243
+
244
+ .btn-primary:hover:not(:disabled) {
245
+ transform: translateY(-3px);
246
+ box-shadow: 0 5px 20px rgba(254, 44, 85, 0.4);
247
+ }
248
+
249
+ .btn-secondary {
250
+ background: var(--bg-hover);
251
+ color: var(--text-primary);
252
+ }
253
+
254
+ .btn-secondary:hover:not(:disabled) {
255
+ background: #3a3a4e;
256
+ }
257
+
258
+ .btn:disabled {
259
+ opacity: 0.5;
260
+ cursor: not-allowed;
261
+ }
262
+
263
+ .progress-section {
264
+ margin-top: 30px;
265
+ display: none;
266
+ }
267
+
268
+ .progress-header {
269
+ display: flex;
270
+ justify-content: space-between;
271
+ align-items: center;
272
+ margin-bottom: 15px;
273
+ }
274
+
275
+ .progress-title {
276
+ font-size: 1.1rem;
277
+ color: var(--text-primary);
278
+ }
279
+
280
+ .progress-stats {
281
+ display: flex;
282
+ gap: 20px;
283
+ font-size: 0.9rem;
284
+ }
285
+
286
+ .stat {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 5px;
290
+ }
291
+
292
+ .stat.success { color: var(--success); }
293
+ .stat.error { color: var(--error); }
294
+ .stat.pending { color: var(--warning); }
295
+
296
+ .progress-bar {
297
+ width: 100%;
298
+ height: 8px;
299
+ background: var(--bg-dark);
300
+ border-radius: 10px;
301
+ overflow: hidden;
302
+ margin-bottom: 20px;
303
+ }
304
+
305
+ .progress-fill {
306
+ height: 100%;
307
+ background: var(--gradient);
308
+ border-radius: 10px;
309
+ transition: width 0.3s ease;
310
+ width: 0%;
311
+ }
312
+
313
+ .downloads-list {
314
+ max-height: 400px;
315
+ overflow-y: auto;
316
+ margin-top: 20px;
317
+ }
318
+
319
+ .download-item {
320
+ background: var(--bg-dark);
321
+ border: 1px solid var(--border);
322
+ border-radius: 12px;
323
+ padding: 15px;
324
+ margin-bottom: 15px;
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 15px;
328
+ transition: all 0.3s;
329
+ }
330
+
331
+ .download-item:hover {
332
+ border-color: var(--primary);
333
+ transform: translateX(5px);
334
+ }
335
+
336
+ .download-status {
337
+ width: 40px;
338
+ height: 40px;
339
+ border-radius: 50%;
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: center;
343
+ flex-shrink: 0;
344
+ }
345
+
346
+ .status-pending {
347
+ background: rgba(255, 170, 0, 0.2);
348
+ color: var(--warning);
349
+ }
350
+
351
+ .status-processing {
352
+ background: rgba(37, 244, 238, 0.2);
353
+ color: var(--secondary);
354
+ animation: spin 1s linear infinite;
355
+ }
356
+
357
+ @keyframes spin {
358
+ from { transform: rotate(0deg); }
359
+ to { transform: rotate(360deg); }
360
+ }
361
+
362
+ .status-success {
363
+ background: rgba(0, 255, 136, 0.2);
364
+ color: var(--success);
365
+ }
366
+
367
+ .status-error {
368
+ background: rgba(255, 51, 102, 0.2);
369
+ color: var(--error);
370
+ }
371
+
372
+ .download-info {
373
+ flex: 1;
374
+ }
375
+
376
+ .download-url {
377
+ font-size: 0.9rem;
378
+ color: var(--text-secondary);
379
+ margin-bottom: 5px;
380
+ word-break: break-all;
381
+ }
382
+
383
+ .download-details {
384
+ display: flex;
385
+ gap: 15px;
386
+ font-size: 0.85rem;
387
+ color: var(--text-secondary);
388
+ }
389
+
390
+ .download-actions {
391
+ display: flex;
392
+ gap: 10px;
393
+ }
394
+
395
+ .action-btn {
396
+ width: 35px;
397
+ height: 35px;
398
+ border-radius: 8px;
399
+ border: none;
400
+ background: var(--bg-hover);
401
+ color: var(--text-secondary);
402
+ cursor: pointer;
403
+ display: flex;
404
+ align-items: center;
405
+ justify-content: center;
406
+ transition: all 0.3s;
407
+ }
408
+
409
+ .action-btn:hover {
410
+ background: var(--primary);
411
+ color: white;
412
+ transform: scale(1.1);
413
+ }
414
+
415
+ .notification {
416
+ position: fixed;
417
+ bottom: 20px;
418
+ right: 20px;
419
+ background: var(--bg-card);
420
+ border: 1px solid var(--border);
421
+ border-radius: 12px;
422
+ padding: 15px 20px;
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 15px;
426
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
427
+ transform: translateX(400px);
428
+ transition: transform 0.3s;
429
+ z-index: 1000;
430
+ }
431
+
432
+ .notification.show {
433
+ transform: translateX(0);
434
+ }
435
+
436
+ .notification-icon {
437
+ width: 30px;
438
+ height: 30px;
439
+ border-radius: 50%;
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: center;
443
+ }
444
+
445
+ .notification.success .notification-icon {
446
+ background: rgba(0, 255, 136, 0.2);
447
+ color: var(--success);
448
+ }
449
+
450
+ .notification.error .notification-icon {
451
+ background: rgba(255, 51, 102, 0.2);
452
+ color: var(--error);
453
+ }
454
+
455
+ @media (max-width: 968px) {
456
+ .main-content {
457
+ grid-template-columns: 1fr;
458
+ }
459
+ }
460
+
461
+ .debug-info {
462
+ margin-top: 10px;
463
+ padding: 10px;
464
+ background: rgba(255, 0, 0, 0.1);
465
+ border-radius: 8px;
466
+ font-size: 0.8rem;
467
+ color: var(--error);
468
+ }
469
+
470
+ .method-info {
471
+ background: rgba(37, 244, 238, 0.1);
472
+ border: 1px solid var(--secondary);
473
+ border-radius: 12px;
474
+ padding: 20px;
475
+ margin-top: 20px;
476
+ }
477
+
478
+ .method-info h3 {
479
+ color: var(--secondary);
480
+ margin-bottom: 10px;
481
+ }
482
+
483
+ .method-info p {
484
+ color: var(--text-secondary);
485
+ line-height: 1.6;
486
+ font-size: 0.9rem;
487
+ }
488
+
489
+ .quality-indicator {
490
+ display: inline-block;
491
+ padding: 2px 8px;
492
+ border-radius: 4px;
493
+ font-size: 0.75rem;
494
+ font-weight: bold;
495
+ margin-left: 5px;
496
+ }
497
+
498
+ .quality-hd {
499
+ background: var(--success);
500
+ color: white;
501
+ }
502
+
503
+ .quality-sd {
504
+ background: var(--warning);
505
+ color: white;
506
+ }
507
+ </style>
508
+ </head>
509
+ <body>
510
+ <header>
511
+ <div class="header-content">
512
+ <div class="logo">
513
+ <div class="logo-icon">
514
+ <i class="fas fa-download"></i>
515
+ </div>
516
+ <h1>Douyin Video Downloader</h1>
517
  </div>
518
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with">
519
+ Built with anycoder
520
+ </a>
521
  </div>
522
+ </header>
523
+
524
+ <div class="container">
525
+ <div class="main-content">
526
+ <div class="card">
527
+ <div class="card-header">
528
+ <div class="card-icon">
529
+ <i class="fas fa-link"></i>
530
+ </div>
531
+ <div class="card-title">Adicionar Links do Douyin</div>
532
+ </div>
533
+
534
+ <div class="input-section">
535
+ <label class="input-label">Cole os links dos vídeos (um por linha):</label>
536
+ <textarea
537
+ id="linksInput"
538
+ class="links-textarea"
539
+ placeholder="https://www.douyin.com/video/7123456789012345678&#10;https://v.douyin.com/ABC123&#10;https://www.douyin.com/video/7123456789012345679"
540
+ ></textarea>
541
+ <div id="urlChips" class="url-chips"></div>
542
+ </div>
543
+
544
+ <div class="action-buttons">
545
+ <button class="btn btn-primary" onclick="startDownloads()" id="startBtn">
546
+ <i class="fas fa-play"></i>
547
+ Iniciar Downloads
548
+ </button>
549
+ <button class="btn btn-secondary" onclick="clearLinks()">
550
+ <i class="fas fa-trash"></i>
551
+ Limpar
552
+ </button>
553
+ </div>
554
+
555
+ <div class="progress-section" id="progressSection">
556
+ <div class="progress-header">
557
+ <div class="progress-title">Progresso dos Downloads</div>
558
+ <div class="progress-stats">
559
+ <div class="stat success">
560
+ <i class="fas fa-check-circle"></i>
561
+ <span id="successCount">0</span>
562
+ </div>
563
+ <div class="stat error">
564
+ <i class="fas fa-times-circle"></i>
565
+ <span id="errorCount">0</span>
566
+ </div>
567
+ <div class="stat pending">
568
+ <i class="fas fa-clock"></i>
569
+ <span id="pendingCount">0</span>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ <div class="progress-bar">
574
+ <div class="progress-fill" id="progressFill"></div>
575
+ </div>
576
+ <div class="downloads-list" id="downloadsList"></div>
577
+ </div>
578
+ </div>
579
 
580
+ <div class="card">
581
+ <div class="card-header">
582
+ <div class="card-icon">
583
+ <i class="fas fa-info-circle"></i>
584
+ </div>
585
+ <div class="card-title">Método de Extração</div>
586
+ </div>
587
+
588
+ <div class="method-info">
589
+ <h3><i class="fas fa-rocket"></i> Algoritmo de Alta Qualidade</h3>
590
+ <p><strong>Passo 1:</strong> Extração do video_id do modal_id</p>
591
+ <p><strong>Passo 2:</strong> Geração de hash com base64 + length + 1000</p>
592
+ <p><strong>Passo 3:</strong> Requisição POST para snapdouyin.app API</p>
593
+ <p><strong>Passo 4:</strong> Ordenação por tamanho (maior bitrate primeiro)</p>
594
+ <p><strong>Passo 5:</strong> Extração do link direto de maior qualidade</p>
595
+ <p><strong>Fallback:</strong> Redirecionamento direto se falhar</p>
596
+ </div>
597
+
598
+ <div style="margin-top: 30px; padding: 20px; background: rgba(254, 44, 85, 0.1); border-radius: 12px;">
599
+ <h3 style="color: var(--primary); margin-bottom: 10px;">
600
+ <i class="fas fa-code"></i> Lógica Implementada
601
+ </h3>
602
+ <pre style="background: var(--bg-dark); padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 0.8rem; color: var(--secondary);">
603
+ // 1. Extrair URL base
604
+ const u = url.split('?')[0];
605
+
606
+ // 2. Obter video_id
607
+ const vid = new URLSearchParams(url.split('?')[1]).get('modal_id');
608
+
609
+ // 3. Construir URL alvo
610
+ const targetUrl = vid ? `https://www.douyin.com/video/${vid}` : u;
611
+
612
+ // 4. Gerar hash
613
+ const hash = btoa(targetUrl) + (targetUrl.length + 1000) + btoa('aio-dl');
614
+
615
+ // 5. Ordenar por tamanho
616
+ sortedMedias = data.medias.sort((a, b) => (b.size || 0) - (a.size || 0));
617
+
618
+ // 6. Pegar maior qualidade
619
+ let absoluteBest = sortedMedias[0];</pre>
620
+ </div>
621
+
622
+ <div style="margin-top: 20px;">
623
+ <h3 style="color: var(--text-primary); margin-bottom: 15px;">
624
+ <i class="fas fa-chart-line"></i> Estatísticas
625
+ </h3>
626
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
627
+ <div style="background: var(--bg-dark); padding: 15px; border-radius: 10px;">
628
+ <div style="color: var(--text-secondary); font-size: 0.85rem;">Total Processado</div>
629
+ <div style="font-size: 1.5rem; font-weight: bold; color: var(--primary);" id="totalProcessed">0</div>
630
+ </div>
631
+ <div style="background: var(--bg-dark); padding: 15px; border-radius: 10px;">
632
+ <div style="color: var(--text-secondary); font-size: 0.85rem;">Taxa de Sucesso</div>
633
+ <div style="font-size: 1.5rem; font-weight: bold; color: var(--success);" id="successRate">0%</div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+
638
+ <div style="margin-top: 20px; padding: 15px; background: rgba(255, 170, 0, 0.1); border-radius: 12px;">
639
+ <p style="color: var(--warning); font-size: 0.9rem;">
640
+ <i class="fas fa-exclamation-triangle"></i>
641
+ <strong>Nota:</strong> Este método extrai vídeos na máxima qualidade disponível (106MB+)
642
+ </p>
643
+ </div>
644
+ </div>
645
  </div>
 
646
  </div>
 
647
 
648
+ <div class="notification" id="notification">
649
+ <div class="notification-icon">
650
+ <i class="fas fa-check"></i>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  </div>
652
+ <div class="notification-content">
653
+ <div id="notificationText" style="font-weight: 600;"></div>
 
 
 
 
 
654
  </div>
655
+ </div>
656
+
657
+ <script>
658
+ let downloadQueue = [];
659
+ let processedCount = 0;
660
+ let successCount = 0;
661
+ let errorCount = 0;
662
+
663
+ // Função principal baseada no JavaScript fornecido
664
+ async function extractDouyinVideo(originalUrl) {
665
+ try {
666
+ console.log("🚀 Processando URL:", originalUrl);
667
+
668
+ // 1. Extrair URL base (remover query e hash)
669
+ const u = originalUrl.split('?')[0];
670
+
671
+ // 2. Identificar video_id do modal_id
672
+ const urlParams = new URLSearchParams(originalUrl.split('?')[1] || '');
673
+ const vid = urlParams.get('modal_id');
674
+
675
+ // 3. Construir targetUrl
676
+ const targetUrl = vid ? `https://www.douyin.com/video/${vid}` : u;
677
+ console.log("📍 Target URL:", targetUrl);
678
+
679
+ // 4. Gerar hash conforme algoritmo
680
+ const hash = btoa(targetUrl) + (targetUrl.length + 1000) + btoa('aio-dl');
681
+ console.log("🔐 Hash gerado:", hash.substring(0, 50) + "...");
682
+
683
+ // 5. Simular requisição POST para API
684
+ const response = await fetch('https://snapdouyin.app/wp-json/mx-downloader/video-data/', {
685
+ method: 'POST',
686
+ headers: {
687
+ 'Content-Type': 'application/x-www-form-urlencoded',
688
+ },
689
+ body: `url=${encodeURIComponent(targetUrl)}&hash=${hash}`
690
+ });
691
+
692
+ const data = await response.json();
693
+ console.log("📦 Resposta da API:", data);
694
+
695
+ // 6. Verificar se há medias
696
+ if (data.medias && data.medias.length > 0) {
697
+ // 7. ORDENAÇÃO POR TAMANHO: Garante que o maior venha primeiro
698
+ const sortedMedias = data.medias.sort((a, b) => (b.size || 0) - (a.size || 0));
699
+
700
+ // 8. Pega o primeiro da lista (o maior de todos)
701
+ const absoluteBest = sortedMedias[0];
702
+
703
+ console.log("✅ Maior arquivo encontrado:", absoluteBest.formattedSize, `(${absoluteBest.size} bytes)`);
704
+ console.log("🔗 Link de download:", absoluteBest.url);
705
+
706
+ return {
707
+ success: true,
708
+ downloadUrl: absoluteBest.url,
709
+ quality: absoluteBest.formattedSize || 'HD',
710
+ size: absoluteBest.size || 0,
711
+ title: 'Douyin Video - Highest Quality'
712
+ };
713
+ } else {
714
+ throw new Error('Nenhuma mídia encontrada na resposta');
715
+ }
716
+
717
+ } catch (error) {
718
+ console.log("❌ CORS block ou erro de rede. Usando fallback...");
719
+ console.error("Erro:", error.message);
720
+
721
+ // Gerar fallback URL
722
+ const urlParams = new URLSearchParams(originalUrl.split('?')[1] || '');
723
+ const vid = urlParams.get('modal_id');
724
+ const targetUrl = vid ? `https://www.douyin.com/video/${vid}` : originalUrl.split('?')[0];
725
+
726
+ const fallbackUrl = `https://snapdouyin.app/#url=${encodeURIComponent(targetUrl)}`;
727
+
728
+ return {
729
+ success: false,
730
+ fallbackUrl: fallbackUrl,
731
+ error: error.message
732
+ };
733
+ }
734
+ }
735
+
736
+ function parseLinks() {
737
+ const input = document.getElementById('linksInput').value;
738
+ const urls = input.split('\n')
739
+ .map(url => url.trim())
740
+ .filter(url => url && (url.includes('douyin.com') || url.includes('v.douyin.com')));
741
+
742
+ const uniqueUrls = [...new Set(urls)];
743
+ updateUrlChips(uniqueUrls);
744
+ return uniqueUrls;
745
+ }
746
+
747
+ function updateUrlChips(urls) {
748
+ const chipsContainer = document.getElementById('urlChips');
749
+ chipsContainer.innerHTML = '';
750
+
751
+ urls.forEach((url, index) => {
752
+ const chip = document.createElement('div');
753
+ chip.className = 'url-chip';
754
+ chip.innerHTML = `
755
+ <i class="fas fa-link"></i>
756
+ <span>${url.substring(0, 40)}${url.length > 40 ? '...' : ''}</span>
757
+ <i class="fas fa-times remove" onclick="removeUrl(${index})"></i>
758
+ `;
759
+ chipsContainer.appendChild(chip);
760
+ });
761
+ }
762
+
763
+ function removeUrl(index) {
764
+ const input = document.getElementById('linksInput');
765
+ const urls = input.value.split('\n');
766
+ urls.splice(index, 1);
767
+ input.value = urls.join('\n');
768
+ parseLinks();
769
+ }
770
+
771
+ function clearLinks() {
772
+ document.getElementById('linksInput').value = '';
773
+ document.getElementById('urlChips').innerHTML = '';
774
+ document.getElementById('progressSection').style.display = 'none';
775
+ resetStats();
776
+ }
777
+
778
+ function resetStats() {
779
+ processedCount = 0;
780
+ successCount = 0;
781
+ errorCount = 0;
782
+ downloadQueue = [];
783
+ updateProgressDisplay();
784
+ }
785
+
786
+ function createDownloadItem(url, index) {
787
+ const item = document.createElement('div');
788
+ item.className = 'download-item';
789
+ item.id = `download-${index}`;
790
+ item.innerHTML = `
791
+ <div class="download-status status-pending">
792
+ <i class="fas fa-clock"></i>
793
+ </div>
794
+ <div class="download-info">
795
+ <div class="download-url">${url}</div>
796
+ <div class="download-details">
797
+ <span id="quality-${index}">Aguardando...</span>
798
+ <span id="size-${index}">--</span>
799
+ </div>
800
+ <div id="debug-${index}" class="debug-info" style="display: none;"></div>
801
+ </div>
802
+ <div class="download-actions">
803
+ <button class="action-btn" onclick="copyLink('${index}')" title="Copiar Link">
804
+ <i class="fas fa-copy"></i>
805
+ </button>
806
+ <button class="action-btn" onclick="downloadVideo('${index}')" title="Baixar">
807
+ <i class="fas fa-download"></i>
808
+ </button>
809
+ </div>
810
+ `;
811
+ return item;
812
+ }
813
+
814
+ function updateDownloadStatus(index, status, data = {}) {
815
+ const item = document.getElementById(`download-${index}`);
816
+ if (!item) return;
817
+
818
+ const statusDiv = item.querySelector('.download-status');
819
+ const qualitySpan = document.getElementById(`quality-${index}`);
820
+ const sizeSpan = document.getElementById(`size-${index}`);
821
+ const debugDiv = document.getElementById(`debug-${index}`);
822
+
823
+ statusDiv.className = `download-status status-${status}`;
824
+
825
+ switch(status) {
826
+ case 'processing':
827
+ statusDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
828
+ qualitySpan.textContent = 'Processando...';
829
+ break;
830
+ case 'success':
831
+ statusDiv.innerHTML = '<i class="fas fa-check"></i>';
832
+ qualitySpan.innerHTML = `${data.quality} <span class="quality-indicator quality-hd">HD</span>`;
833
+ sizeSpan.textContent = formatFileSize(data.size);
834
+ break;
835
+ case 'error':
836
+ statusDiv.innerHTML = '<i class="fas fa-times"></i>';
837
+ qualitySpan.textContent = 'Fallback';
838
+ sizeSpan.textContent = 'Link direto';
839
+ if (debugDiv) {
840
+ debugDiv.style.display = 'block';
841
+ debugDiv.innerHTML = `<i class="fas fa-info-circle"></i> Usando fallback: ${data.error}`;
842
+ }
843
+ break;
844
+ }
845
+ }
846
+
847
+ function formatFileSize(bytes) {
848
+ if (!bytes || bytes === 0) return '--';
849
+ const mb = bytes / (1024 * 1024);
850
+ return `${mb.toFixed(1)} MB`;
851
+ }
852
+
853
+ async function processDownload(url, index) {
854
+ updateDownloadStatus(index, 'processing');
855
+
856
+ const result = await extractDouyinVideo(url);
857
+
858
+ if (result.success) {
859
+ downloadQueue[index] = result;
860
+ updateDownloadStatus(index, 'success', result);
861
+ successCount++;
862
+ showNotification('Vídeo extraído com máxima qualidade!', 'success');
863
+ } else {
864
+ downloadQueue[index] = result;
865
+ updateDownloadStatus(index, 'error', result);
866
+ errorCount++;
867
+ showNotification('Usando método fallback', 'error');
868
+ }
869
+
870
+ processedCount++;
871
+ updateProgressDisplay();
872
+ }
873
+
874
+ async function startDownloads() {
875
+ const urls = parseLinks();
876
+
877
+ if (urls.length === 0) {
878
+ showNotification('Por favor, adicione pelo menos um link', 'error');
879
+ return;
880
+ }
881
+
882
+ // Resetar contadores
883
+ processedCount = 0;
884
+ successCount = 0;
885
+ errorCount = 0;
886
+ downloadQueue = new Array(urls.length);
887
+
888
+ // Mostrar seção de progresso
889
+ document.getElementById('progressSection').style.display = 'block';
890
+ document.getElementById('downloadsList').innerHTML = '';
891
+
892
+ // Criar itens de download
893
+ urls.forEach((url, index) => {
894
+ const item = createDownloadItem(url, index);
895
+ document.getElementById('downloadsList').appendChild(item);
896
+ });
897
+
898
+ // Desabilitar botão
899
+ document.getElementById('startBtn').disabled = true;
900
+
901
+ // Processar downloads sequencialmente
902
+ for (let i = 0; i < urls.length; i++) {
903
+ await processDownload(urls[i], i);
904
+ }
905
+
906
+ // Reabilitar botão
907
+ document.getElementById('startBtn').disabled = false;
908
+
909
+ // Notificação final
910
+ const message = `Concluído! ${successCount} de ${urls.length} vídeos processados.`;
911
+ showNotification(message, successCount > 0 ? 'success' : 'error');
912
+ }
913
+
914
+ function updateProgressDisplay() {
915
+ const total = downloadQueue.length;
916
+ const progress = total > 0 ? (processedCount / total) * 100 : 0;
917
+
918
+ document.getElementById('progressFill').style.width = `${progress}%`;
919
+ document.getElementById('successCount').textContent = successCount;
920
+ document.getElementById('errorCount').textContent = errorCount;
921
+ document.getElementById('pendingCount').textContent = total - processedCount;
922
+ document.getElementById('totalProcessed').textContent = processedCount;
923
+
924
+ const successRate = total > 0 ? Math.round((successCount / total) * 100) : 0;
925
+ document.getElementById('successRate').textContent = `${successRate}%`;
926
+ }
927
+
928
+ function copyLink(index) {
929
+ const data = downloadQueue[index];
930
+ if (data) {
931
+ const url = data.success ? data.downloadUrl : data.fallbackUrl;
932
+ navigator.clipboard.writeText(url);
933
+ showNotification('Link copiado!', 'success');
934
+ }
935
+ }
936
+
937
+ function downloadVideo(index) {
938
+ const data = downloadQueue[index];
939
+ if (data) {
940
+ const url = data.success ? data.downloadUrl : data.fallbackUrl;
941
+ if (url.startsWith('https://snapdouyin.app/#')) {
942
+ // Fallback - abrir em nova aba
943
+ window.open(url, '_blank');
944
+ } else {
945
+ // Download direto
946
+ const a = document.createElement('a');
947
+ a.href = url;
948
+ a.download = `douyin_video_${index}.mp4`;
949
+ a.target = '_blank';
950
+ document.body.appendChild(a);
951
+ a.click();
952
+ document.body.removeChild(a);
953
+ }
954
+ showNotification('Download iniciado!', 'success');
955
+ }
956
+ }
957
+
958
+ function showNotification(message, type = 'success') {
959
+ const notification = document.getElementById('notification');
960
+ const icon = notification.querySelector('.notification-icon i');
961
+ const text = document.getElementById('notificationText');
962
+
963
+ notification.className = `notification ${type}`;
964
+ icon.className = type === 'success' ?
965
+ 'fas fa-check' : 'fas fa-exclamation-triangle';
966
+ text.textContent = message;
967
+
968
+ notification.classList.add('show');
969
+
970
+ setTimeout(() => {
971
+ notification.classList.remove('show');
972
+ }, 3000);
973
+ }
974
+
975
+ // Auto-parse ao digitar
976
+ document.getElementById('linksInput').addEventListener('input', parseLinks);
977
+ </script>
978
+ </body>
979
+ </html>