eubottura commited on
Commit
db99bc6
Β·
verified Β·
1 Parent(s): 619a990

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +450 -139
index.html CHANGED
@@ -4,7 +4,7 @@
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Douyin Video Download Link Processor</title>
8
  <style>
9
  * {
10
  margin: 0;
@@ -25,6 +25,7 @@
25
  --success: #4ade80;
26
  --error: #f87171;
27
  --warning: #fbbf24;
 
28
  }
29
 
30
  body {
@@ -43,7 +44,7 @@
43
  left: 0;
44
  right: 0;
45
  bottom: 0;
46
- background:
47
  radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.1) 0%, transparent 50%),
48
  radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.1) 0%, transparent 50%),
49
  radial-gradient(circle at 40% 20%, rgba(255, 0, 80, 0.05) 0%, transparent 50%);
@@ -311,6 +312,10 @@
311
  background: var(--error);
312
  }
313
 
 
 
 
 
314
  .quality-badge {
315
  padding: 0.2rem 0.6rem;
316
  background: rgba(74, 222, 128, 0.2);
@@ -326,6 +331,11 @@
326
  color: var(--error);
327
  }
328
 
 
 
 
 
 
329
  .error-message {
330
  background: rgba(248, 113, 113, 0.1);
331
  border: 1px solid rgba(248, 113, 113, 0.3);
@@ -340,12 +350,25 @@
340
  display: block;
341
  }
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  @keyframes fadeInDown {
344
  from {
345
  opacity: 0;
346
  transform: translateY(-30px);
347
  }
348
-
349
  to {
350
  opacity: 1;
351
  transform: translateY(0);
@@ -357,7 +380,6 @@
357
  opacity: 0;
358
  transform: translateY(30px);
359
  }
360
-
361
  to {
362
  opacity: 1;
363
  transform: translateY(0);
@@ -369,7 +391,6 @@
369
  opacity: 0;
370
  transform: translateX(-20px);
371
  }
372
-
373
  to {
374
  opacity: 1;
375
  transform: translateX(0);
@@ -377,24 +398,18 @@
377
  }
378
 
379
  @keyframes pulse {
380
-
381
- 0%,
382
- 100% {
383
  transform: scale(1);
384
  }
385
-
386
  50% {
387
  transform: scale(1.1);
388
  }
389
  }
390
 
391
  @keyframes blink {
392
-
393
- 0%,
394
- 100% {
395
  opacity: 1;
396
  }
397
-
398
  50% {
399
  opacity: 0.5;
400
  }
@@ -446,27 +461,71 @@
446
  color: var(--warning);
447
  }
448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  @media (max-width: 768px) {
450
  .container {
451
  padding: 1rem;
452
  }
453
-
454
  h1 {
455
  font-size: 2rem;
456
  }
457
-
458
  .main-card {
459
  padding: 1.5rem;
460
  }
461
-
462
  .button-group {
463
  flex-direction: column;
464
  }
465
-
466
  button {
467
  width: 100%;
468
  }
469
-
470
  .results-header {
471
  flex-direction: column;
472
  gap: 1rem;
@@ -479,21 +538,27 @@
479
  <body>
480
  <div class="container">
481
  <header>
482
- <h1>Douyin Video Download Link Processor</h1>
483
- <p class="subtitle">Extract highest-bitrate V3 direct download links</p>
484
- <p class="credit">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a>
485
- </p>
486
  </header>
487
 
488
  <main class="main-card">
489
  <div class="cors-notice">
490
- ⚠️ <strong>CORS Notice:</strong> Due to browser security restrictions, direct API calls may be blocked. The app will
491
- automatically use fallback URLs when CORS errors occur.
 
 
 
492
  </div>
493
 
494
  <div class="info-box">
495
- πŸ“Œ <strong>Processing Logic:</strong> Extracts modal_id, generates API hash, sorts by size (largest first), and
496
- returns the V3 download link. Falls back to snapdouyin.app if CORS error occurs.
 
 
 
 
497
  </div>
498
 
499
  <section class="input-section">
@@ -503,23 +568,27 @@
503
  placeholder="https://www.douyin.com/video/7123456789012345678?modal_id=7123456789012345678&#10;https://v.douyin.com/ABCDEFG123456/&#10;https://www.douyin.com/user/987654321?modal_id=7123456789012345678"
504
  ></textarea>
505
  <div class="button-group">
506
- <button onclick="processUrls()">Process URLs</button>
507
- <button class="secondary" onclick="clearInput()">Clear</button>
508
- <button class="secondary" onclick="loadSample()">Load Sample URLs</button>
 
509
  </div>
510
  </section>
511
 
512
  <div class="loading" id="loading">
513
  <div class="spinner"></div>
514
- <p>πŸš€ Processing URLs...</p>
515
  </div>
516
 
517
  <div class="error-message" id="errorMessage"></div>
 
 
 
518
 
519
  <section class="results-section" id="resultsSection">
520
  <div class="results-header">
521
- <h2 class="results-title">Download Links <span class="status-indicator success"></span></h2>
522
- <button class="copy-all-btn" onclick="copyAllUrls()">Copy All</button>
523
  </div>
524
  <ul class="url-list" id="urlList"></ul>
525
  </section>
@@ -527,9 +596,31 @@
527
  </div>
528
 
529
  <script>
530
- // Base64 encoding function
531
- function base64Encode(str) {
532
- return btoa(unescape(encodeURIComponent(str)));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  }
534
 
535
  // URL encoding function
@@ -537,98 +628,264 @@
537
  return encodeURIComponent(str);
538
  }
539
 
540
- // Process a single URL following the exact JavaScript logic
541
- async function processSingleUrl(inputUrl) {
542
- try {
543
- console.log(`πŸ” Processing: ${inputUrl}`);
544
-
545
- // Step a: Extract target URL by removing query and hash
546
- const u = inputUrl.split('?')[0];
547
-
548
- // Step b: Extract modal_id from search params
549
- const vid = new URLSearchParams(inputUrl.split('?')[1] || '').get('modal_id');
550
-
551
- // Step b: Construct targetUrl
552
- const targetUrl = vid ? `https://www.douyin.com/video/${vid}` : u;
553
-
554
- console.log(`πŸ“ Target URL: ${targetUrl}`);
555
-
556
- // Step c: Generate API hash using exact formula
557
- const hash = base64Encode(targetUrl) + (targetUrl.length + 1000) + base64Encode('aio-dl');
558
-
559
- console.log(`πŸ” Generated hash: ${hash.substring(0, 50)}...`);
560
-
561
- // Step d: Make real API call to snapdouyin.app endpoint
562
- const endpoint = 'https://snapdouyin.app/wp-json/mx-downloader/video-data/';
563
- const requestBody = `url=${urlEncode(targetUrl)}&hash=${hash}`;
564
-
565
- console.log(`πŸ“‘ Making POST to: ${endpoint}`);
566
- console.log(`πŸ“¦ Body: ${requestBody.substring(0, 100)}...`);
567
-
568
- // Make the actual fetch request
569
- const response = await fetch(endpoint, {
570
- method: 'POST',
571
- headers: {
572
- 'Content-Type': 'application/x-www-form-urlencoded',
573
- },
574
- body: requestBody
575
- });
576
-
577
- if (!response.ok) {
578
- throw new Error(`HTTP error! status: ${response.status}`);
579
- }
580
-
581
- const data = await response.json();
582
- console.log(`βœ… API Response received:`, data);
583
-
584
- // Step e: Check if medias array exists
585
- if (data.medias && data.medias.length > 0) {
586
- console.log(`βœ… Found ${data.medias.length} media options`);
587
 
588
- // Step f: Sort medias by size in descending order (largest first)
589
- const sortedMedias = data.medias.sort((a, b) => (b.size || 0) - (a.size || 0));
 
 
 
 
 
 
590
 
591
- // Step f: Get the first (largest) media
592
- const absoluteBest = sortedMedias[0];
 
593
 
594
- console.log(`πŸ† Best media: ${absoluteBest.formattedSize || (absoluteBest.size + ' bytes')}`);
 
595
 
596
- // Step g: Extract the download URL (V3 link)
597
- const downloadUrl = absoluteBest.url;
 
 
 
 
 
 
 
 
 
 
 
 
 
598
 
599
- console.log(`🎯 Extracted V3 link: ${downloadUrl}`);
 
 
 
 
 
 
 
600
 
601
- // Return the V3 download link with metadata
602
- return {
603
- url: downloadUrl,
604
- size: absoluteBest.size,
605
- formattedSize: absoluteBest.formattedSize || (absoluteBest.size ? `${(absoluteBest.size / 1000000).toFixed(1)} MB` : 'Unknown'),
606
- quality: absoluteBest.quality || 'Unknown',
607
- isV3: true
608
- };
609
- } else {
610
- console.log("❌ No media found in response");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
- // Step h: Handle errors with fallback URL
613
- const fallbackUrl = `https://snapdouyin.app/#url=${urlEncode(targetUrl)}`;
 
 
 
 
 
614
 
615
- console.log(`πŸ”„ Using fallback: ${fallbackUrl}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
 
617
- return {
618
- url: fallbackUrl,
619
- isFallback: true
620
- };
 
 
 
 
 
621
  }
622
  } catch (error) {
623
- console.log(`πŸ’₯ Error occurred: ${error.message}`);
624
- console.log("πŸ”„ Using fallback due to CORS block or network error");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
 
626
- // Fallback on any error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  const fallbackUrl = `https://snapdouyin.app/#url=${urlEncode(inputUrl)}`;
 
 
 
 
 
 
628
 
 
 
 
629
  return {
630
  url: fallbackUrl,
631
  isFallback: true,
 
632
  error: error.message
633
  };
634
  }
@@ -648,26 +905,38 @@
648
  return;
649
  }
650
 
651
- // Hide error message
652
- hideError();
653
-
654
- // Show loading
655
  document.getElementById('loading').classList.add('show');
656
  document.getElementById('resultsSection').classList.remove('show');
 
657
 
658
  const results = [];
 
659
 
660
- // Process URLs sequentially
661
  for (let i = 0; i < urls.length; i++) {
662
  const url = urls[i].trim();
 
 
663
  try {
664
  const result = await processSingleUrl(url);
665
  results.push(result);
 
 
 
 
 
 
 
 
 
666
  } catch (error) {
667
- console.error(`Failed to process URL ${i + 1}:`, error);
668
  results.push({
669
  url: `https://snapdouyin.app/#url=${urlEncode(url)}`,
670
  isFallback: true,
 
671
  error: error.message
672
  });
673
  }
@@ -675,11 +944,17 @@
675
 
676
  // Hide loading and show results
677
  document.getElementById('loading').classList.remove('show');
678
- displayResults(results);
 
 
 
 
 
 
679
  }
680
 
681
- // Display results with enhanced UI
682
- function displayResults(results) {
683
  const urlList = document.getElementById('urlList');
684
  urlList.innerHTML = '';
685
 
@@ -689,24 +964,36 @@
689
  li.style.animationDelay = `${index * 0.1}s`;
690
 
691
  const isFallback = result.isFallback;
692
- const isV3 = result.isV3;
693
 
694
  // Create quality badge
695
  let qualityBadge = '';
 
 
696
  if (isV3) {
697
- qualityBadge = `<span class="quality-badge">V3 ${result.quality} β€’ ${result.formattedSize}</span>`;
698
  } else if (isFallback) {
699
- qualityBadge = '<span class="quality-badge fallback">Fallback</span>';
 
 
 
700
  }
701
 
702
- // Add URL content class for V3 links
 
 
 
 
 
 
703
  const urlContentClass = isV3 ? 'url-content v3-link' : 'url-content';
704
 
705
  li.innerHTML = `
706
  <div class="${urlContentClass}">${result.url}</div>
707
  ${qualityBadge}
 
708
  <button class="copy-btn" onclick="copyUrl('${result.url.replace(/'/g, "\\'")}', this)">Copy</button>
709
- <span class="status-indicator ${isFallback ? 'fallback' : 'success'}"></span>
710
  `;
711
 
712
  urlList.appendChild(li);
@@ -718,7 +1005,7 @@
718
  // Copy single URL
719
  function copyUrl(url, button) {
720
  navigator.clipboard.writeText(url).then(() => {
721
- button.textContent = 'Copied!';
722
  button.classList.add('copied');
723
  setTimeout(() => {
724
  button.textContent = 'Copy';
@@ -737,7 +1024,7 @@
737
  navigator.clipboard.writeText(text).then(() => {
738
  const button = document.querySelector('.copy-all-btn');
739
  const originalText = button.textContent;
740
- button.textContent = 'Copied All!';
741
  button.style.background = 'var(--success)';
742
  setTimeout(() => {
743
  button.textContent = originalText;
@@ -752,40 +1039,64 @@
752
  function clearInput() {
753
  document.getElementById('urlInput').value = '';
754
  document.getElementById('resultsSection').classList.remove('show');
755
- hideError();
 
 
 
 
756
  }
757
 
758
  // Load sample URLs
759
  function loadSample() {
760
- const sampleUrls = `https://www.douyin.com/video/7123456789012345678?modal_id=7123456789012345678
761
- https://v.douyin.com/ABCDEFG123456/
762
- https://www.douyin.com/user/987654321?modal_id=7123456789012345678`;
 
763
  document.getElementById('urlInput').value = sampleUrls;
764
  }
765
 
766
  // Show error message
767
  function showError(message) {
 
768
  const errorElement = document.getElementById('errorMessage');
769
- errorElement.textContent = message;
770
  errorElement.classList.add('show');
771
  }
772
 
773
- // Hide error message
774
- function hideError() {
775
- const errorElement = document.getElementById('errorMessage');
776
- errorElement.classList.remove('show');
 
 
 
 
 
 
 
 
 
777
  }
778
 
779
- // Add enter key support for processing
780
  document.getElementById('urlInput').addEventListener('keydown', function(e) {
781
  if (e.ctrlKey && e.key === 'Enter') {
782
  processUrls();
783
  }
 
 
 
 
 
 
 
 
784
  });
785
 
786
- // Console log for debugging
787
- console.log('πŸš€ Douyin Video Download Link Processor Ready');
788
- console.log('πŸ“ Processing logic initialized - Making real API calls to snapdouyin.app');
 
789
  </script>
790
  </body>
791
 
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Douyin V3 Video Downloader Pro</title>
8
  <style>
9
  * {
10
  margin: 0;
 
25
  --success: #4ade80;
26
  --error: #f87171;
27
  --warning: #fbbf24;
28
+ --info: #60a5fa;
29
  }
30
 
31
  body {
 
44
  left: 0;
45
  right: 0;
46
  bottom: 0;
47
+ background:
48
  radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.1) 0%, transparent 50%),
49
  radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.1) 0%, transparent 50%),
50
  radial-gradient(circle at 40% 20%, rgba(255, 0, 80, 0.05) 0%, transparent 50%);
 
312
  background: var(--error);
313
  }
314
 
315
+ .status-indicator.processing {
316
+ background: var(--warning);
317
+ }
318
+
319
  .quality-badge {
320
  padding: 0.2rem 0.6rem;
321
  background: rgba(74, 222, 128, 0.2);
 
331
  color: var(--error);
332
  }
333
 
334
+ .quality-badge.processing {
335
+ background: rgba(251, 191, 36, 0.2);
336
+ color: var(--warning);
337
+ }
338
+
339
  .error-message {
340
  background: rgba(248, 113, 113, 0.1);
341
  border: 1px solid rgba(248, 113, 113, 0.3);
 
350
  display: block;
351
  }
352
 
353
+ .info-message {
354
+ background: rgba(96, 165, 250, 0.1);
355
+ border: 1px solid rgba(96, 165, 250, 0.3);
356
+ border-radius: 10px;
357
+ padding: 1rem;
358
+ margin-top: 1rem;
359
+ color: var(--info);
360
+ display: none;
361
+ }
362
+
363
+ .info-message.show {
364
+ display: block;
365
+ }
366
+
367
  @keyframes fadeInDown {
368
  from {
369
  opacity: 0;
370
  transform: translateY(-30px);
371
  }
 
372
  to {
373
  opacity: 1;
374
  transform: translateY(0);
 
380
  opacity: 0;
381
  transform: translateY(30px);
382
  }
 
383
  to {
384
  opacity: 1;
385
  transform: translateY(0);
 
391
  opacity: 0;
392
  transform: translateX(-20px);
393
  }
 
394
  to {
395
  opacity: 1;
396
  transform: translateX(0);
 
398
  }
399
 
400
  @keyframes pulse {
401
+ 0%, 100% {
 
 
402
  transform: scale(1);
403
  }
 
404
  50% {
405
  transform: scale(1.1);
406
  }
407
  }
408
 
409
  @keyframes blink {
410
+ 0%, 100% {
 
 
411
  opacity: 1;
412
  }
 
413
  50% {
414
  opacity: 0.5;
415
  }
 
461
  color: var(--warning);
462
  }
463
 
464
+ .success-notice {
465
+ background: rgba(74, 222, 128, 0.1);
466
+ border: 1px solid rgba(74, 222, 128, 0.3);
467
+ border-radius: 10px;
468
+ padding: 1rem;
469
+ margin-bottom: 1.5rem;
470
+ font-size: 0.9rem;
471
+ color: var(--success);
472
+ display: none;
473
+ }
474
+
475
+ .success-notice.show {
476
+ display: block;
477
+ }
478
+
479
+ .log-box {
480
+ background: rgba(0, 0, 0, 0.3);
481
+ border: 1px solid var(--border-color);
482
+ border-radius: 10px;
483
+ padding: 1rem;
484
+ margin: 1rem 0;
485
+ font-family: 'Courier New', monospace;
486
+ font-size: 0.85rem;
487
+ max-height: 200px;
488
+ overflow-y: auto;
489
+ display: none;
490
+ }
491
+
492
+ .log-box.show {
493
+ display: block;
494
+ }
495
+
496
+ .log-entry {
497
+ margin: 0.25rem 0;
498
+ color: var(--text-secondary);
499
+ }
500
+
501
+ .log-entry.success {
502
+ color: var(--success);
503
+ }
504
+
505
+ .log-entry.error {
506
+ color: var(--error);
507
+ }
508
+
509
+ .log-entry.info {
510
+ color: var(--info);
511
+ }
512
+
513
  @media (max-width: 768px) {
514
  .container {
515
  padding: 1rem;
516
  }
 
517
  h1 {
518
  font-size: 2rem;
519
  }
 
520
  .main-card {
521
  padding: 1.5rem;
522
  }
 
523
  .button-group {
524
  flex-direction: column;
525
  }
 
526
  button {
527
  width: 100%;
528
  }
 
529
  .results-header {
530
  flex-direction: column;
531
  gap: 1rem;
 
538
  <body>
539
  <div class="container">
540
  <header>
541
+ <h1>Douyin V3 Video Downloader Pro</h1>
542
+ <p class="subtitle">Extract highest-quality V3 direct download links</p>
543
+ <p class="credit">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a></p>
 
544
  </header>
545
 
546
  <main class="main-card">
547
  <div class="cors-notice">
548
+ ⚠️ <strong>Enhanced Processing:</strong> Uses multiple API endpoints and advanced extraction techniques to get V3 links.
549
+ </div>
550
+
551
+ <div class="success-notice" id="successNotice">
552
+ βœ… <strong>Success!</strong> V3 links extracted successfully.
553
  </div>
554
 
555
  <div class="info-box">
556
+ πŸ“Œ <strong>Processing Strategy:</strong>
557
+ <br>1. Extract video ID from URL
558
+ <br>2. Try multiple API endpoints
559
+ <br>3. Parse response for V3 links
560
+ <br>4. Sort by quality (highest first)
561
+ <br>5. Return direct download URL
562
  </div>
563
 
564
  <section class="input-section">
 
568
  placeholder="https://www.douyin.com/video/7123456789012345678?modal_id=7123456789012345678&#10;https://v.douyin.com/ABCDEFG123456/&#10;https://www.douyin.com/user/987654321?modal_id=7123456789012345678"
569
  ></textarea>
570
  <div class="button-group">
571
+ <button onclick="processUrls()">πŸš€ Process URLs</button>
572
+ <button class="secondary" onclick="clearInput()">πŸ—‘οΈ Clear</button>
573
+ <button class="secondary" onclick="loadSample()">πŸ“‹ Load Sample</button>
574
+ <button class="secondary" onclick="toggleDebug()">πŸ”§ Debug</button>
575
  </div>
576
  </section>
577
 
578
  <div class="loading" id="loading">
579
  <div class="spinner"></div>
580
+ <p>πŸ” Extracting V3 links...</p>
581
  </div>
582
 
583
  <div class="error-message" id="errorMessage"></div>
584
+ <div class="info-message" id="infoMessage"></div>
585
+
586
+ <div class="log-box" id="logBox"></div>
587
 
588
  <section class="results-section" id="resultsSection">
589
  <div class="results-header">
590
+ <h2 class="results-title">V3 Download Links <span class="status-indicator success"></span></h2>
591
+ <button class="copy-all-btn" onclick="copyAllUrls()">πŸ“‹ Copy All</button>
592
  </div>
593
  <ul class="url-list" id="urlList"></ul>
594
  </section>
 
596
  </div>
597
 
598
  <script>
599
+ let debugMode = false;
600
+
601
+ // Logger function
602
+ function log(message, type = 'info') {
603
+ if (!debugMode) return;
604
+
605
+ const logBox = document.getElementById('logBox');
606
+ const entry = document.createElement('div');
607
+ entry.className = `log-entry ${type}`;
608
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
609
+ logBox.appendChild(entry);
610
+ logBox.scrollTop = logBox.scrollHeight;
611
+ console.log(`[${type.toUpperCase()}] ${message}`);
612
+ }
613
+
614
+ // Toggle debug mode
615
+ function toggleDebug() {
616
+ debugMode = !debugMode;
617
+ const logBox = document.getElementById('logBox');
618
+ if (debugMode) {
619
+ logBox.classList.add('show');
620
+ log('Debug mode enabled', 'info');
621
+ } else {
622
+ logBox.classList.remove('show');
623
+ }
624
  }
625
 
626
  // URL encoding function
 
628
  return encodeURIComponent(str);
629
  }
630
 
631
+ // Base64 encoding
632
+ function base64Encode(str) {
633
+ return btoa(unescape(encodeURIComponent(str)));
634
+ }
635
+
636
+ // Extract video ID from Douyin URL
637
+ function extractVideoId(url) {
638
+ log(`Extracting video ID from: ${url}`, 'info');
639
+
640
+ // Try to get modal_id from query params
641
+ const urlObj = new URL(url);
642
+ const modalId = urlObj.searchParams.get('modal_id');
643
+
644
+ if (modalId) {
645
+ log(`Found modal_id: ${modalId}`, 'success');
646
+ return modalId;
647
+ }
648
+
649
+ // Extract from path
650
+ const pathMatch = url.match(/\/video\/(\d+)/);
651
+ if (pathMatch) {
652
+ log(`Found video ID in path: ${pathMatch[1]}`, 'success');
653
+ return pathMatch[1];
654
+ }
655
+
656
+ // For short URLs, we'll need to fetch and redirect
657
+ log('No video ID found, might be short URL', 'error');
658
+ return null;
659
+ }
660
+
661
+ // Process with snapdouyin API
662
+ async function processWithSnapdouyin(videoId, originalUrl) {
663
+ log(`Trying snapdouyin API for video ID: ${videoId}`, 'info');
664
+
665
+ const targetUrl = `https://www.douyin.com/video/${videoId}`;
666
+ const hash = base64Encode(targetUrl) + (targetUrl.length + 1000) + base64Encode('aio-dl');
667
+
668
+ const endpoints = [
669
+ 'https://snapdouyin.app/wp-json/mx-downloader/video-data/',
670
+ 'https://snapdouyin.app/wp-json/aio-video-downloader/v1/video-info/',
671
+ 'https://snapdouyin.app/api/video-info'
672
+ ];
673
+
674
+ for (const endpoint of endpoints) {
675
+ try {
676
+ log(`Trying endpoint: ${endpoint}`, 'info');
 
677
 
678
+ const response = await fetch(endpoint, {
679
+ method: 'POST',
680
+ headers: {
681
+ 'Content-Type': 'application/x-www-form-urlencoded',
682
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
683
+ },
684
+ body: `url=${urlEncode(targetUrl)}&hash=${hash}`
685
+ });
686
 
687
+ if (!response.ok) {
688
+ throw new Error(`HTTP ${response.status}`);
689
+ }
690
 
691
+ const data = await response.json();
692
+ log(`Response received: ${JSON.stringify(data).substring(0, 200)}...`, 'info');
693
 
694
+ if (data.medias && data.medias.length > 0) {
695
+ const sortedMedias = data.medias.sort((a, b) => (b.size || 0) - (a.size || 0));
696
+ const best = sortedMedias[0];
697
+
698
+ if (best.url && best.url.includes('v3')) {
699
+ log(`Found V3 link: ${best.url}`, 'success');
700
+ return {
701
+ url: best.url,
702
+ size: best.size,
703
+ formattedSize: best.formattedSize || `${(best.size / 1000000).toFixed(1)} MB`,
704
+ quality: best.quality || 'V3',
705
+ source: 'snapdouyin'
706
+ };
707
+ }
708
+ }
709
 
710
+ // Try other response formats
711
+ if (data.video_url) {
712
+ log(`Found video_url: ${data.video_url}`, 'success');
713
+ return {
714
+ url: data.video_url,
715
+ source: 'snapdouyin-alt'
716
+ };
717
+ }
718
 
719
+ } catch (error) {
720
+ log(`Endpoint ${endpoint} failed: ${error.message}`, 'error');
721
+ }
722
+ }
723
+
724
+ return null;
725
+ }
726
+
727
+ // Process with alternative API
728
+ async function processWithAlternative(videoId) {
729
+ log(`Trying alternative API for video ID: ${videoId}`, 'info');
730
+
731
+ const endpoints = [
732
+ 'https://api.douyin.wtf/api',
733
+ 'https://douyin.wtf/api',
734
+ 'https://tikmate.online/api/douyin'
735
+ ];
736
+
737
+ for (const endpoint of endpoints) {
738
+ try {
739
+ log(`Trying alternative endpoint: ${endpoint}`, 'info');
740
+
741
+ const response = await fetch(`${endpoint}?url=https://www.douyin.com/video/${videoId}`, {
742
+ method: 'GET',
743
+ headers: {
744
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
745
+ }
746
+ });
747
+
748
+ if (!response.ok) {
749
+ throw new Error(`HTTP ${response.status}`);
750
+ }
751
+
752
+ const data = await response.json();
753
+ log(`Alternative API response: ${JSON.stringify(data).substring(0, 200)}...`, 'info');
754
+
755
+ // Check different response formats
756
+ if (data.data && data.data.play_url) {
757
+ log(`Found play_url in alternative API`, 'success');
758
+ return {
759
+ url: data.data.play_url,
760
+ source: 'alternative'
761
+ };
762
+ }
763
 
764
+ if (data.video_url) {
765
+ log(`Found video_url in alternative API`, 'success');
766
+ return {
767
+ url: data.video_url,
768
+ source: 'alternative'
769
+ };
770
+ }
771
 
772
+ } catch (error) {
773
+ log(`Alternative endpoint ${endpoint} failed: ${error.message}`, 'error');
774
+ }
775
+ }
776
+
777
+ return null;
778
+ }
779
+
780
+ // Process with direct page scraping simulation
781
+ async function processWithDirectScraping(videoId) {
782
+ log(`Trying direct scraping approach for video ID: ${videoId}`, 'info');
783
+
784
+ try {
785
+ // Try to get video info from public API
786
+ const apiUrl = `https://www.iesdouyin.com/share/video/${videoId}/?region=CN&mid=${videoId}`;
787
+
788
+ const response = await fetch(apiUrl, {
789
+ method: 'GET',
790
+ headers: {
791
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15',
792
+ 'Referer': 'https://www.douyin.com/'
793
+ }
794
+ });
795
+
796
+ if (response.ok) {
797
+ const text = await response.text();
798
+ log(`Page content received, length: ${text.length}`, 'info');
799
 
800
+ // Try to extract video URL from page content
801
+ const urlMatch = text.match(/"play_addr":\s*{\s*"url_list":\s*\["([^"]+)"/);
802
+ if (urlMatch && urlMatch[1]) {
803
+ log(`Extracted URL from page: ${urlMatch[1]}`, 'success');
804
+ return {
805
+ url: urlMatch[1].replace(/\\/g, ''),
806
+ source: 'direct-scraping'
807
+ };
808
+ }
809
  }
810
  } catch (error) {
811
+ log(`Direct scraping failed: ${error.message}`, 'error');
812
+ }
813
+
814
+ return null;
815
+ }
816
+
817
+ // Main processing function for a single URL
818
+ async function processSingleUrl(inputUrl) {
819
+ log(`\n=== Processing URL: ${inputUrl} ===`, 'info');
820
+
821
+ try {
822
+ // Step 1: Extract video ID
823
+ let videoId = extractVideoId(inputUrl);
824
+
825
+ // If it's a short URL, try to resolve it
826
+ if (!videoId && inputUrl.includes('v.douyin.com')) {
827
+ log('Attempting to resolve short URL', 'info');
828
+ try {
829
+ // Note: This won't work due to CORS, but we try anyway
830
+ const response = await fetch(inputUrl, {
831
+ method: 'HEAD',
832
+ redirect: 'manual'
833
+ });
834
+ const location = response.headers.get('location');
835
+ if (location) {
836
+ videoId = extractVideoId(location);
837
+ log(`Resolved short URL to: ${location}`, 'success');
838
+ }
839
+ } catch (error) {
840
+ log('Could not resolve short URL due to CORS', 'error');
841
+ }
842
+ }
843
 
844
+ if (!videoId) {
845
+ throw new Error('Could not extract video ID');
846
+ }
847
+
848
+ // Step 2: Try different methods to get V3 link
849
+ let result = null;
850
+
851
+ // Method 1: snapdouyin API
852
+ result = await processWithSnapdouyin(videoId, inputUrl);
853
+ if (result) {
854
+ log('Success with snapdouyin API', 'success');
855
+ return result;
856
+ }
857
+
858
+ // Method 2: Alternative APIs
859
+ result = await processWithAlternative(videoId);
860
+ if (result) {
861
+ log('Success with alternative API', 'success');
862
+ return result;
863
+ }
864
+
865
+ // Method 3: Direct scraping
866
+ result = await processWithDirectScraping(videoId);
867
+ if (result) {
868
+ log('Success with direct scraping', 'success');
869
+ return result;
870
+ }
871
+
872
+ // If all methods fail, return fallback
873
+ log('All methods failed, using fallback', 'error');
874
  const fallbackUrl = `https://snapdouyin.app/#url=${urlEncode(inputUrl)}`;
875
+ return {
876
+ url: fallbackUrl,
877
+ isFallback: true,
878
+ source: 'fallback',
879
+ error: 'Could not extract V3 link'
880
+ };
881
 
882
+ } catch (error) {
883
+ log(`Processing failed: ${error.message}`, 'error');
884
+ const fallbackUrl = `https://snapdouyin.app/#url=${urlEncode(inputUrl)}`;
885
  return {
886
  url: fallbackUrl,
887
  isFallback: true,
888
+ source: 'error-fallback',
889
  error: error.message
890
  };
891
  }
 
905
  return;
906
  }
907
 
908
+ // Reset UI
909
+ hideMessages();
 
 
910
  document.getElementById('loading').classList.add('show');
911
  document.getElementById('resultsSection').classList.remove('show');
912
+ document.getElementById('successNotice').classList.remove('show');
913
 
914
  const results = [];
915
+ let successCount = 0;
916
 
917
+ // Process URLs with delay to avoid rate limiting
918
  for (let i = 0; i < urls.length; i++) {
919
  const url = urls[i].trim();
920
+ log(`\nπŸ”„ Processing URL ${i + 1}/${urls.length}`, 'info');
921
+
922
  try {
923
  const result = await processSingleUrl(url);
924
  results.push(result);
925
+
926
+ if (!result.isFallback) {
927
+ successCount++;
928
+ }
929
+
930
+ // Add delay between requests
931
+ if (i < urls.length - 1) {
932
+ await new Promise(resolve => setTimeout(resolve, 1000));
933
+ }
934
  } catch (error) {
935
+ log(`Failed to process URL ${i + 1}: ${error.message}`, 'error');
936
  results.push({
937
  url: `https://snapdouyin.app/#url=${urlEncode(url)}`,
938
  isFallback: true,
939
+ source: 'catch-fallback',
940
  error: error.message
941
  });
942
  }
 
944
 
945
  // Hide loading and show results
946
  document.getElementById('loading').classList.remove('show');
947
+ displayResults(results, successCount);
948
+
949
+ // Show success notice if we got any V3 links
950
+ if (successCount > 0) {
951
+ document.getElementById('successNotice').classList.add('show');
952
+ showInfo(`Successfully extracted ${successCount} V3 link(s) out of ${urls.length} URLs`);
953
+ }
954
  }
955
 
956
+ // Display results
957
+ function displayResults(results, successCount) {
958
  const urlList = document.getElementById('urlList');
959
  urlList.innerHTML = '';
960
 
 
964
  li.style.animationDelay = `${index * 0.1}s`;
965
 
966
  const isFallback = result.isFallback;
967
+ const isV3 = !isFallback && (result.url.includes('v3') || result.quality === 'V3');
968
 
969
  // Create quality badge
970
  let qualityBadge = '';
971
+ let statusClass = 'success';
972
+
973
  if (isV3) {
974
+ qualityBadge = `<span class="quality-badge">βœ“ V3 ${result.quality || 'HD'} β€’ ${result.formattedSize || 'Unknown'}</span>`;
975
  } else if (isFallback) {
976
+ qualityBadge = '<span class="quality-badge fallback">βœ— Fallback</span>';
977
+ statusClass = 'fallback';
978
+ } else {
979
+ qualityBadge = `<span class="quality-badge">βœ“ ${result.quality || 'Unknown'}</span>`;
980
  }
981
 
982
+ // Add source info
983
+ let sourceInfo = '';
984
+ if (result.source) {
985
+ sourceInfo = `<small style="color: var(--text-secondary);">[${result.source}]</small>`;
986
+ }
987
+
988
+ // Add URL content class
989
  const urlContentClass = isV3 ? 'url-content v3-link' : 'url-content';
990
 
991
  li.innerHTML = `
992
  <div class="${urlContentClass}">${result.url}</div>
993
  ${qualityBadge}
994
+ ${sourceInfo}
995
  <button class="copy-btn" onclick="copyUrl('${result.url.replace(/'/g, "\\'")}', this)">Copy</button>
996
+ <span class="status-indicator ${statusClass}"></span>
997
  `;
998
 
999
  urlList.appendChild(li);
 
1005
  // Copy single URL
1006
  function copyUrl(url, button) {
1007
  navigator.clipboard.writeText(url).then(() => {
1008
+ button.textContent = 'βœ“ Copied!';
1009
  button.classList.add('copied');
1010
  setTimeout(() => {
1011
  button.textContent = 'Copy';
 
1024
  navigator.clipboard.writeText(text).then(() => {
1025
  const button = document.querySelector('.copy-all-btn');
1026
  const originalText = button.textContent;
1027
+ button.textContent = 'βœ“ Copied All!';
1028
  button.style.background = 'var(--success)';
1029
  setTimeout(() => {
1030
  button.textContent = originalText;
 
1039
  function clearInput() {
1040
  document.getElementById('urlInput').value = '';
1041
  document.getElementById('resultsSection').classList.remove('show');
1042
+ hideMessages();
1043
+ if (debugMode) {
1044
+ document.getElementById('logBox').innerHTML = '';
1045
+ log('Debug mode enabled', 'info');
1046
+ }
1047
  }
1048
 
1049
  // Load sample URLs
1050
  function loadSample() {
1051
+ const sampleUrls = `https://www.douyin.com/video/7300000012345678901?modal_id=7300000012345678901
1052
+ https://v.douyin.com/ieFQDjC/
1053
+ https://www.douyin.com/video/7300000023456789012
1054
+ https://www.douyin.com/user/MS4wLjABAAAA5rOKyL98?modal_id=7300000034567890123`;
1055
  document.getElementById('urlInput').value = sampleUrls;
1056
  }
1057
 
1058
  // Show error message
1059
  function showError(message) {
1060
+ hideMessages();
1061
  const errorElement = document.getElementById('errorMessage');
1062
+ errorElement.textContent = `❌ ${message}`;
1063
  errorElement.classList.add('show');
1064
  }
1065
 
1066
+ // Show info message
1067
+ function showInfo(message) {
1068
+ hideMessages();
1069
+ const infoElement = document.getElementById('infoMessage');
1070
+ infoElement.textContent = `ℹ️ ${message}`;
1071
+ infoElement.classList.add('show');
1072
+ }
1073
+
1074
+ // Hide all messages
1075
+ function hideMessages() {
1076
+ document.getElementById('errorMessage').classList.remove('show');
1077
+ document.getElementById('infoMessage').classList.remove('show');
1078
+ document.getElementById('successNotice').classList.remove('show');
1079
  }
1080
 
1081
+ // Add keyboard shortcuts
1082
  document.getElementById('urlInput').addEventListener('keydown', function(e) {
1083
  if (e.ctrlKey && e.key === 'Enter') {
1084
  processUrls();
1085
  }
1086
+ if (e.ctrlKey && e.key === 'l') {
1087
+ e.preventDefault();
1088
+ clearInput();
1089
+ }
1090
+ if (e.ctrlKey && e.key === 'd') {
1091
+ e.preventDefault();
1092
+ toggleDebug();
1093
+ }
1094
  });
1095
 
1096
+ // Initialize
1097
+ console.log('πŸš€ Douyin V3 Video Downloader Pro Ready');
1098
+ console.log('πŸ“ Enhanced processing with multiple API endpoints');
1099
+ console.log('πŸ’‘ Press Ctrl+D to toggle debug mode');
1100
  </script>
1101
  </body>
1102