Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Douyin Video Downloader</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary: #000000; | |
| --primary-hover: #333333; | |
| --secondary: #F8F8F8; | |
| --background: #FFFFFF; | |
| --surface: #FFFFFF; | |
| --text-primary: #000000; | |
| --text-secondary: #666666; | |
| --text-tertiary: #999999; | |
| --border: #E5E5E5; | |
| --border-hover: #CCCCCC; | |
| --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); | |
| --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06); | |
| --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08); | |
| --radius: 8px; | |
| --radius-lg: 12px; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'Inter', sans-serif; | |
| background: var(--background); | |
| min-height: 100vh; | |
| color: var(--text-primary); | |
| line-height: 1.5; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| .container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 4rem 2rem; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 3rem; | |
| } | |
| h1 { | |
| font-size: 2rem; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| margin-bottom: 0.5rem; | |
| color: var(--text-primary); | |
| } | |
| .subtitle { | |
| color: var(--text-secondary); | |
| font-size: 0.95rem; | |
| font-weight: 400; | |
| } | |
| .credit { | |
| margin-top: 2rem; | |
| font-size: 0.8rem; | |
| color: var(--text-tertiary); | |
| } | |
| .credit a { | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| transition: color 0.2s ease; | |
| } | |
| .credit a:hover { | |
| color: var(--text-primary); | |
| text-decoration: underline; | |
| } | |
| .main-card { | |
| background: var(--surface); | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow-sm); | |
| padding: 2.5rem; | |
| } | |
| .notice { | |
| padding: 1rem; | |
| border-radius: var(--radius); | |
| font-size: 0.875rem; | |
| margin-bottom: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .cors-notice { | |
| background: #F8F8F8; | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| } | |
| .success-notice { | |
| background: #F0F9F0; | |
| border: 1px solid #D0E7D0; | |
| color: #2D5A2D; | |
| display: none; | |
| } | |
| .info-box { | |
| background: #F8F9FA; | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| } | |
| .input-section { | |
| margin-bottom: 2rem; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| font-size: 0.9rem; | |
| color: var(--text-primary); | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 140px; | |
| padding: 0.875rem; | |
| background: var(--secondary); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| color: var(--text-primary); | |
| font-size: 0.875rem; | |
| font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; | |
| resize: vertical; | |
| transition: all 0.2s ease; | |
| line-height: 1.5; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: var(--text-primary); | |
| background: var(--surface); | |
| box-shadow: 0 0 0 1px var(--text-primary); | |
| } | |
| textarea::placeholder { | |
| color: var(--text-tertiary); | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| flex-wrap: wrap; | |
| } | |
| button { | |
| padding: 0.625rem 1.25rem; | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| border-radius: var(--radius); | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| button:hover { | |
| background: var(--primary-hover); | |
| } | |
| button.secondary { | |
| background: var(--secondary); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border); | |
| } | |
| button.secondary:hover { | |
| background: var(--surface); | |
| border-color: var(--border-hover); | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 2rem; | |
| } | |
| .loading.show { | |
| display: block; | |
| } | |
| .spinner { | |
| width: 32px; | |
| height: 32px; | |
| border: 2px solid var(--border); | |
| border-top-color: var(--text-primary); | |
| border-radius: 50%; | |
| margin: 0 auto 1rem; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .message { | |
| padding: 0.875rem; | |
| border-radius: var(--radius); | |
| font-size: 0.875rem; | |
| margin-top: 1rem; | |
| display: none; | |
| } | |
| .error-message { | |
| background: #FFF5F5; | |
| border: 1px solid #FED7D7; | |
| color: #742A2A; | |
| } | |
| .info-message { | |
| background: #F0F7FF; | |
| border: 1px solid #BEE3F8; | |
| color: #2A4365; | |
| } | |
| .log-box { | |
| background: var(--secondary); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 0.875rem; | |
| margin: 1rem 0; | |
| font-family: 'SF Mono', 'Monaco', monospace; | |
| font-size: 0.75rem; | |
| max-height: 180px; | |
| overflow-y: auto; | |
| display: none; | |
| } | |
| .log-entry { | |
| margin: 0.25rem 0; | |
| color: var(--text-secondary); | |
| } | |
| .log-entry.success { | |
| color: #2D5A2D; | |
| } | |
| .log-entry.error { | |
| color: #742A2A; | |
| } | |
| .log-entry.info { | |
| color: #2A4365; | |
| } | |
| .results-section { | |
| margin-top: 2rem; | |
| opacity: 0; | |
| transform: translateY(10px); | |
| transition: all 0.3s ease; | |
| } | |
| .results-section.show { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| .results-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| padding-bottom: 0.75rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .results-title { | |
| font-size: 1.125rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .copy-all-btn { | |
| padding: 0.5rem 1rem; | |
| font-size: 0.8rem; | |
| } | |
| .url-list { | |
| list-style: none; | |
| } | |
| .url-item { | |
| padding: 1rem; | |
| margin-bottom: 0.5rem; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| transition: all 0.2s ease; | |
| animation: slideIn 0.3s ease forwards; | |
| opacity: 0; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| .url-item:hover { | |
| border-color: var(--border-hover); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .url-content { | |
| flex: 1; | |
| word-break: break-all; | |
| font-family: 'SF Mono', 'Monaco', monospace; | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| } | |
| .url-content.v3-link { | |
| color: var(--text-primary); | |
| font-weight: 500; | |
| } | |
| .copy-btn { | |
| padding: 0.375rem 0.75rem; | |
| font-size: 0.75rem; | |
| background: var(--secondary); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border); | |
| min-width: auto; | |
| } | |
| .copy-btn.copied { | |
| background: #2D5A2D; | |
| color: white; | |
| border-color: #2D5A2D; | |
| } | |
| .status-indicator { | |
| width: 5px; | |
| height: 5px; | |
| border-radius: 50%; | |
| margin-left: 0.5rem; | |
| } | |
| .status-indicator.success { | |
| background: #2D5A2D; | |
| } | |
| .status-indicator.fallback { | |
| background: #742A2A; | |
| } | |
| .status-indicator.processing { | |
| background: #744210; | |
| } | |
| .quality-badge { | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.7rem; | |
| font-weight: 500; | |
| margin-left: 0.5rem; | |
| } | |
| .quality-badge { | |
| background: #F0F9F0; | |
| color: #2D5A2D; | |
| } | |
| .quality-badge.fallback { | |
| background: #FFF5F5; | |
| color: #742A2A; | |
| } | |
| .quality-badge.processing { | |
| background: #FFFBF0; | |
| color: #744210; | |
| } | |
| @media (max-width: 640px) { | |
| .container { | |
| padding: 2rem 1rem; | |
| } | |
| h1 { | |
| font-size: 1.75rem; | |
| } | |
| .main-card { | |
| padding: 1.5rem; | |
| } | |
| .button-group { | |
| flex-direction: column; | |
| } | |
| button { | |
| width: 100%; | |
| } | |
| .results-header { | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| align-items: stretch; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Video Downloader</h1> | |
| <p class="subtitle">Extract direct download links</p> | |
| <p class="credit">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </p> | |
| </header> | |
| <main class="main-card"> | |
| <div class="notice cors-notice"> | |
| <strong>Enhanced Processing:</strong> Multiple extraction methods applied. | |
| </div> | |
| <div class="notice success-notice" id="successNotice"> | |
| <strong>Success!</strong> Links extracted successfully. | |
| </div> | |
| <div class="notice info-box"> | |
| <strong>Processing Strategy:</strong> | |
| <br>1. Extract video ID | |
| <br>2. Try multiple API endpoints | |
| <br>3. Parse for download links | |
| <br>4. Sort by quality | |
| </div> | |
| <section class="input-section"> | |
| <label for="urlInput">Enter Video URLs (one per line):</label> | |
| <textarea | |
| id="urlInput" | |
| placeholder="https://www.douyin.com/video/7123456789012345678 https://v.douyin.com/ABCDEFG123456/ https://www.douyin.com/user/987654321?modal_id=7123456789012345678" | |
| ></textarea> | |
| <div class="button-group"> | |
| <button onclick="processUrls()">Process URLs</button> | |
| <button class="secondary" onclick="clearInput()">Clear</button> | |
| <button class="secondary" onclick="loadSample()">Load Sample</button> | |
| <button class="secondary" onclick="toggleDebug()">Debug</button> | |
| </div> | |
| </section> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Extracting links...</p> | |
| </div> | |
| <div class="message error-message" id="errorMessage"></div> | |
| <div class="message info-message" id="infoMessage"></div> | |
| <div class="log-box" id="logBox"></div> | |
| <section class="results-section" id="resultsSection"> | |
| <div class="results-header"> | |
| <h2 class="results-title"> | |
| Download Links | |
| <span class="status-indicator success"></span> | |
| </h2> | |
| <button class="copy-all-btn" onclick="copyAllUrls()">Copy All</button> | |
| </div> | |
| <ul class="url-list" id="urlList"></ul> | |
| </section> | |
| </main> | |
| </div> | |
| <script> | |
| let debugMode = false; | |
| function log(message, type = 'info') { | |
| if (!debugMode) return; | |
| const logBox = document.getElementById('logBox'); | |
| const entry = document.createElement('div'); | |
| entry.className = `log-entry ${type}`; | |
| entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; | |
| logBox.appendChild(entry); | |
| logBox.scrollTop = logBox.scrollHeight; | |
| console.log(`[${type.toUpperCase()}] ${message}`); | |
| } | |
| function toggleDebug() { | |
| debugMode = !debugMode; | |
| const logBox = document.getElementById('logBox'); | |
| if (debugMode) { | |
| logBox.classList.add('show'); | |
| logBox.style.display = 'block'; | |
| log('Debug mode enabled', 'info'); | |
| } else { | |
| logBox.classList.remove('show'); | |
| logBox.style.display = 'none'; | |
| } | |
| } | |
| function urlEncode(str) { | |
| return encodeURIComponent(str); | |
| } | |
| function base64Encode(str) { | |
| return btoa(unescape(encodeURIComponent(str))); | |
| } | |
| function extractVideoId(url) { | |
| log(`Extracting video ID from: ${url}`, 'info'); | |
| const urlObj = new URL(url); | |
| const modalId = urlObj.searchParams.get('modal_id'); | |
| if (modalId) { | |
| log(`Found modal_id: ${modalId}`, 'success'); | |
| return modalId; | |
| } | |
| const pathMatch = url.match(/\/video\/(\d+)/); | |
| if (pathMatch) { | |
| log(`Found video ID in path: ${pathMatch[1]}`, 'success'); | |
| return pathMatch[1]; | |
| } | |
| log('No video ID found, might be short URL', 'error'); | |
| return null; | |
| } | |
| async function processWithSnapdouyin(videoId, originalUrl) { | |
| log(`Trying snapdouyin API for video ID: ${videoId}`, 'info'); | |
| const targetUrl = `https://www.douyin.com/video/${videoId}`; | |
| const hash = base64Encode(targetUrl) + (targetUrl.length + 1000) + base64Encode('aio-dl'); | |
| const endpoints = [ | |
| 'https://snapdouyin.app/wp-json/mx-downloader/video-data/', | |
| 'https://snapdouyin.app/wp-json/aio-video-downloader/v1/video-info/', | |
| 'https://snapdouyin.app/api/video-info' | |
| ]; | |
| for (const endpoint of endpoints) { | |
| try { | |
| log(`Trying endpoint: ${endpoint}`, 'info'); | |
| const response = await fetch(endpoint, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/x-www-form-urlencoded', | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | |
| }, | |
| body: `url=${urlEncode(targetUrl)}&hash=${hash}` | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| log(`Response received: ${JSON.stringify(data).substring(0, 200)}...`, 'info'); | |
| if (data.medias && data.medias.length > 0) { | |
| const sortedMedias = data.medias.sort((a, b) => (b.size || 0) - (a.size || 0)); | |
| const best = sortedMedias[0]; | |
| if (best.url && best.url.includes('v3')) { | |
| log(`Found V3 link: ${best.url}`, 'success'); | |
| return { | |
| url: best.url, | |
| size: best.size, | |
| formattedSize: best.formattedSize || `${(best.size / 1000000).toFixed(1)} MB`, | |
| quality: best.quality || 'V3', | |
| source: 'snapdouyin' | |
| }; | |
| } | |
| } | |
| if (data.video_url) { | |
| log(`Found video_url: ${data.video_url}`, 'success'); | |
| return { | |
| url: data.video_url, | |
| source: 'snapdouyin-alt' | |
| }; | |
| } | |
| } catch (error) { | |
| log(`Endpoint ${endpoint} failed: ${error.message}`, 'error'); | |
| } | |
| } | |
| return null; | |
| } | |
| async function processWithAlternative(videoId) { | |
| log(`Trying alternative API for video ID: ${videoId}`, 'info'); | |
| const endpoints = [ | |
| 'https://api.douyin.wtf/api', | |
| 'https://douyin.wtf/api', | |
| 'https://tikmate.online/api/douyin' | |
| ]; | |
| for (const endpoint of endpoints) { | |
| try { | |
| log(`Trying alternative endpoint: ${endpoint}`, 'info'); | |
| const response = await fetch(`${endpoint}?url=https://www.douyin.com/video/${videoId}`, { | |
| method: 'GET', | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| log(`Alternative API response: ${JSON.stringify(data).substring(0, 200)}...`, 'info'); | |
| if (data.data && data.data.play_url) { | |
| log(`Found play_url in alternative API`, 'success'); | |
| return { | |
| url: data.data.play_url, | |
| source: 'alternative' | |
| }; | |
| } | |
| if (data.video_url) { | |
| log(`Found video_url in alternative API`, 'success'); | |
| return { | |
| url: data.video_url, | |
| source: 'alternative' | |
| }; | |
| } | |
| } catch (error) { | |
| log(`Alternative endpoint ${endpoint} failed: ${error.message}`, 'error'); | |
| } | |
| } | |
| return null; | |
| } | |
| async function processWithDirectScraping(videoId) { | |
| log(`Trying direct scraping approach for video ID: ${videoId}`, 'info'); | |
| try { | |
| const apiUrl = `https://www.iesdouyin.com/share/video/${videoId}/?region=CN&mid=${videoId}`; | |
| const response = await fetch(apiUrl, { | |
| method: 'GET', | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15', | |
| 'Referer': 'https://www.douyin.com/' | |
| } | |
| }); | |
| if (response.ok) { | |
| const text = await response.text(); | |
| log(`Page content received, length: ${text.length}`, 'info'); | |
| const urlMatch = text.match(/"play_addr":\s*{\s*"url_list":\s*\["([^"]+)"/); | |
| if (urlMatch && urlMatch[1]) { | |
| log(`Extracted URL from page: ${urlMatch[1]}`, 'success'); | |
| return { | |
| url: urlMatch[1].replace(/\\/g, ''), | |
| source: 'direct-scraping' | |
| }; | |
| } | |
| } | |
| } catch (error) { | |
| log(`Direct scraping failed: ${error.message}`, 'error'); | |
| } | |
| return null; | |
| } | |
| async function processSingleUrl(inputUrl) { | |
| log(`\n=== Processing URL: ${inputUrl} ===`, 'info'); | |
| try { | |
| let videoId = extractVideoId(inputUrl); | |
| if (!videoId && inputUrl.includes('v.douyin.com')) { | |
| log('Attempting to resolve short URL', 'info'); | |
| try { | |
| const response = await fetch(inputUrl, { | |
| method: 'HEAD', | |
| redirect: 'manual' | |
| }); | |
| const location = response.headers.get('location'); | |
| if (location) { | |
| videoId = extractVideoId(location); | |
| log(`Resolved short URL to: ${location}`, 'success'); | |
| } | |
| } catch (error) { | |
| log('Could not resolve short URL due to CORS', 'error'); | |
| } | |
| } | |
| if (!videoId) { | |
| throw new Error('Could not extract video ID'); | |
| } | |
| let result = null; | |
| result = await processWithSnapdouyin(videoId, inputUrl); | |
| if (result) { | |
| log('Success with snapdouyin API', 'success'); | |
| return result; | |
| } | |
| result = await processWithAlternative(videoId); | |
| if (result) { | |
| log('Success with alternative API', 'success'); | |
| return result; | |
| } | |
| result = await processWithDirectScraping(videoId); | |
| if (result) { | |
| log('Success with direct scraping', 'success'); | |
| return result; | |
| } | |
| log('All methods failed, using fallback', 'error'); | |
| const fallbackUrl = `https://snapdouyin.app/#url=${urlEncode(inputUrl)}`; | |
| return { | |
| url: fallbackUrl, | |
| isFallback: true, | |
| source: 'fallback', | |
| error: 'Could not extract V3 link' | |
| }; | |
| } catch (error) { | |
| log(`Processing failed: ${error.message}`, 'error'); | |
| const fallbackUrl = `https://snapdouyin.app/#url=${urlEncode(inputUrl)}`; | |
| return { | |
| url: fallbackUrl, | |
| isFallback: true, | |
| source: 'error-fallback', | |
| error: error.message | |
| }; | |
| } | |
| } | |
| async function processUrls() { | |
| const input = document.getElementById('urlInput').value.trim(); | |
| if (!input) { | |
| showError('Please enter at least one Douyin URL'); | |
| return; | |
| } | |
| const urls = input.split('\n').filter(url => url.trim()); | |
| if (urls.length === 0) { | |
| showError('Please enter valid URLs'); | |
| return; | |
| } | |
| hideMessages(); | |
| document.getElementById('loading').classList.add('show'); | |
| document.getElementById('resultsSection').classList.remove('show'); | |
| document.getElementById('successNotice').classList.remove('show'); | |
| const results = []; | |
| let successCount = 0; | |
| for (let i = 0; i < urls.length; i++) { | |
| const url = urls[i].trim(); | |
| log(`\nπ Processing URL ${i + 1}/${urls.length}`, 'info'); | |
| try { | |
| const result = await processSingleUrl(url); | |
| results.push(result); | |
| if (!result.isFallback) { | |
| successCount++; | |
| } | |
| if (i < urls.length - 1) { | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| } catch (error) { | |
| log(`Failed to process URL ${i + 1}: ${error.message}`, 'error'); | |
| results.push({ | |
| url: `https://snapdouyin.app/#url=${urlEncode(url)}`, | |
| isFallback: true, | |
| source: 'catch-fallback', | |
| error: error.message | |
| }); | |
| } | |
| } | |
| document.getElementById('loading').classList.remove('show'); | |
| displayResults(results, successCount); | |
| if (successCount > 0) { | |
| document.getElementById('successNotice').classList.add('show'); | |
| document.getElementById('successNotice').style.display = 'flex'; | |
| showInfo(`Successfully extracted ${successCount} link(s) out of ${urls.length} URLs`); | |
| } | |
| } | |
| function displayResults(results, successCount) { | |
| const urlList = document.getElementById('urlList'); | |
| urlList.innerHTML = ''; | |
| results.forEach((result, index) => { | |
| const li = document.createElement('li'); | |
| li.className = 'url-item'; | |
| li.style.animationDelay = `${index * 0.05}s}`; | |
| const isFallback = result.isFallback; | |
| const isV3 = !isFallback && (result.url.includes('v3') || result.quality === 'V3'); | |
| let qualityBadge = ''; | |
| let statusClass = 'success'; | |
| if (isV3) { | |
| qualityBadge = `<span class="quality-badge">β V3 ${result.quality || 'HD'} β’ ${result.formattedSize || 'Unknown'}</span>`; | |
| } else if (isFallback) { | |
| qualityBadge = '<span class="quality-badge fallback">β Fallback</span>'; | |
| statusClass = 'fallback'; | |
| } else { | |
| qualityBadge = `<span class="quality-badge">β ${result.quality || 'Unknown'}</span>`; | |
| } | |
| let sourceInfo = ''; | |
| if (result.source) { | |
| sourceInfo = `<small style="color: var(--text-tertiary); font-size: 0.7rem;">[${result.source}]</small>`; | |
| } | |
| const urlContentClass = isV3 ? 'url-content v3-link' : 'url-content'; | |
| li.innerHTML = ` | |
| <div class="${urlContentClass}">${result.url}</div> | |
| ${qualityBadge} | |
| ${sourceInfo} | |
| <button class="copy-btn" onclick="copyUrl('${result.url.replace(/'/g, "\\'")}', this)">Copy</button> | |
| <span class="status-indicator ${statusClass}"></span> | |
| `; | |
| urlList.appendChild(li); | |
| }); | |
| document.getElementById('resultsSection').classList.add('show'); | |
| } | |
| function copyUrl(url, button) { | |
| navigator.clipboard.writeText(url).then(() => { | |
| button.textContent = 'β Copied!'; | |
| button.classList.add('copied'); | |
| setTimeout(() => { | |
| button.textContent = 'Copy'; | |
| button.classList.remove('copied'); | |
| }, 2000); | |
| }).catch(err => { | |
| console.error('Failed to copy:', err); | |
| }); | |
| } | |
| function copyAllUrls() { | |
| const urls = Array.from(document.querySelectorAll('.url-content')).map(el => el.textContent); | |
| const text = urls.map((url, index) => `${index + 1}. ${url}`).join('\n'); | |
| navigator.clipboard.writeText(text).then(() => { | |
| const button = document.querySelector('.copy-all-btn'); | |
| const originalText = button.textContent; | |
| button.textContent = 'β Copied All!'; | |
| button.style.background = '#2D5A2D'; | |
| button.style.color = 'white'; | |
| setTimeout(() => { | |
| button.textContent = originalText; | |
| button.style.background = ''; | |
| button.style.color = ''; | |
| }, 2000); | |
| }).catch(err => { | |
| console.error('Failed to copy all:', err); | |
| }); | |
| } | |
| function clearInput() { | |
| document.getElementById('urlInput').value = ''; | |
| document.getElementById('resultsSection').classList.remove('show'); | |
| hideMessages(); | |
| if (debugMode) { | |
| document.getElementById('logBox').innerHTML = ''; | |
| log('Debug mode enabled', 'info'); | |
| } | |
| } | |
| function loadSample() { | |
| const sampleUrls = `https://www.douyin.com/video/7300000012345678901?modal_id=7300000012345678901 | |
| https://v.douyin.com/ieFQDjC/ | |
| https://www.douyin.com/video/7300000023456789012 | |
| https://www.douyin.com/user/MS4wLjABAAAA5rOKyL98?modal_id=7300000034567890123`; | |
| document.getElementById('urlInput').value = sampleUrls; | |
| } | |
| function showError(message) { | |
| hideMessages(); | |
| const errorElement = document.getElementById('errorMessage'); | |
| errorElement.textContent = message; | |
| errorElement.style.display = 'block'; | |
| } | |
| function showInfo(message) { | |
| hideMessages(); | |
| const infoElement = document.getElementById('infoMessage'); | |
| infoElement.textContent = message; | |
| infoElement.style.display = 'block'; | |
| } | |
| function hideMessages() { | |
| document.getElementById('errorMessage').style.display = 'none'; | |
| document.getElementById('infoMessage').style.display = 'none'; | |
| document.getElementById('successNotice').classList.remove('show'); | |
| document.getElementById('successNotice').style.display = 'none'; | |
| } | |
| document.getElementById('urlInput').addEventListener('keydown', function(e) { | |
| if (e.ctrlKey && e.key === 'Enter') { | |
| processUrls(); | |
| } | |
| if (e.ctrlKey && e.key === 'l') { | |
| e.preventDefault(); | |
| clearInput(); | |
| } | |
| if (e.ctrlKey && e.key === 'd') { | |
| e.preventDefault(); | |
| toggleDebug(); | |
| } | |
| }); | |
| console.log('Video Downloader Ready'); | |
| </script> | |
| </body> | |
| </html> |