File size: 17,411 Bytes
ff2aab1
f19a86d
99e6686
f4cb92f
 
c472c8d
f4cb92f
 
1f42c01
f4cb92f
1f42c01
c472c8d
1f42c01
c472c8d
f4cb92f
1f42c01
6628799
 
c472c8d
1f42c01
f4cb92f
6628799
f4cb92f
 
 
1f42c01
f4cb92f
 
1f42c01
f4cb92f
 
 
 
 
1f42c01
6628799
f4cb92f
 
 
 
 
 
 
6628799
f4cb92f
1f42c01
f4cb92f
6628799
f4cb92f
6628799
c472c8d
f4cb92f
 
 
 
 
 
c472c8d
 
1f42c01
f4cb92f
 
99e6686
 
f4cb92f
1b166b2
f4cb92f
 
 
 
1b166b2
f4cb92f
99e6686
f4cb92f
c472c8d
 
 
 
 
495355e
c472c8d
 
 
 
 
99e6686
c472c8d
 
 
 
1b166b2
c472c8d
 
 
 
 
 
 
f4cb92f
 
c472c8d
 
1b166b2
 
c472c8d
 
 
 
 
 
 
1b166b2
f1a8907
f4cb92f
f1a8907
c472c8d
99e6686
f4cb92f
6628799
c472c8d
 
1f42c01
 
c472c8d
 
 
 
 
1f42c01
 
c472c8d
1f42c01
c472c8d
 
1f42c01
f4cb92f
 
1f42c01
 
c472c8d
 
 
f4cb92f
c472c8d
1f42c01
c472c8d
99e6686
c472c8d
99e6686
c472c8d
 
1f42c01
c472c8d
 
1f42c01
6628799
f4cb92f
1f42c01
 
 
c472c8d
f4cb92f
1f42c01
c472c8d
 
 
 
 
 
 
 
 
 
 
1f42c01
f4cb92f
1f42c01
167e4f3
6628799
 
 
 
 
f4cb92f
89e2b21
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Universal Downloader</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        :root { --primary: #fe2c55; --secondary: #25f4ee; --bg-dark: #161823; --bg-card: #1f1f2e; --bg-hover: #2a2a3e; --text-primary: #ffffff; --text-secondary: #a0a0b0; --border: #2a2a3e; --success: #00ff88; --warning: #ffaa00; --error: #ff3366; --tube5s-accent: #bd93f9; }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 100%); color: var(--text-primary); min-height: 100vh; }
        .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
        header { background: rgba(31, 31, 46, 0.8); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border); padding: 20px 0; position: sticky; top: 0; z-index: 100; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); }
        .header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; padding: 0 20px; }
        .logo { display: flex; align-items: center; gap: 15px; }
        .logo-icon { width: 50px; height: 50px; background: linear-gradient(135deg, #fe2c55 0%, #25f4ee 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; animation: pulse 2s infinite; }
        h1 { font-size: 1.8rem; font-weight: 700; background: linear-gradient(135deg, #fe2c55 0%, #25f4ee 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
        @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
        .main-content { display: grid; grid-template-columns: 1fr; gap: 30px; margin-top: 30px; }
        .card { background: rgba(31, 31, 46, 0.6); backdrop-filter: blur(10px); border: 1px solid var(--border); border-radius: 20px; padding: 30px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); }
        .card-header { display: flex; align-items: center; gap: 15px; margin-bottom: 25px; }
        .card-icon { width: 45px; height: 45px; background: linear-gradient(135deg, #fe2c55 0%, #25f4ee 100%); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; }
        .card-title { font-size: 1.3rem; font-weight: 600; }
        .input-section { margin-bottom: 25px; }
        .input-label { display: block; margin-bottom: 10px; color: var(--text-secondary); font-weight: 500; }
        .links-textarea { width: 100%; min-height: 200px; padding: 15px; background: var(--bg-dark); border: 2px solid var(--border); border-radius: 12px; color: var(--text-primary); font-size: 14px; resize: vertical; transition: border-color 0.3s; }
        .links-textarea:focus { outline: none; border-color: var(--primary); }
        .url-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
        .url-chip { background: var(--bg-hover); padding: 6px 12px; border-radius: 20px; font-size: 12px; color: var(--text-secondary); display: flex; align-items: center; gap: 8px; animation: fadeIn 0.3s; border: 1px solid transparent; }
        .url-chip.type-tube5s { border-color: var(--tube5s-accent); color: var(--tube5s-accent); background: rgba(189, 147, 249, 0.1); }
        @keyframes fadeIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } }
        .url-chip .remove { cursor: pointer; color: var(--error); transition: transform 0.2s; }
        .url-chip .remove:hover { transform: scale(1.2); }
        .action-buttons { display: flex; gap: 15px; margin-top: 20px; }
        .btn { flex: 1; padding: 14px 24px; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 10px; }
        .btn-primary { background: linear-gradient(135deg, #fe2c55 0%, #25f4ee 100%); color: white; }
        .btn-primary:hover:not(:disabled) { transform: translateY(-3px); box-shadow: 0 5px 20px rgba(254, 44, 85, 0.4); }
        .btn-secondary { background: var(--bg-hover); color: var(--text-primary); }
        .btn-secondary:hover:not(:disabled) { background: #3a3a4e; }
        .btn:disabled { opacity: 0.5; cursor: not-allowed; }
        .progress-section { margin-top: 30px; display: none; }
        .progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
        .progress-bar { width: 100%; height: 8px; background: var(--bg-dark); border-radius: 10px; overflow: hidden; margin-bottom: 20px; }
        .progress-fill { height: 100%; background: linear-gradient(135deg, #fe2c55 0%, #25f4ee 100%); border-radius: 10px; transition: width 0.3s ease; width: 0%; }
        .downloads-list { max-height: 400px; overflow-y: auto; margin-top: 20px; }
        .download-item { background: var(--bg-dark); border: 1px solid var(--border); border-radius: 12px; padding: 15px; margin-bottom: 15px; display: flex; align-items: center; gap: 15px; transition: all 0.3s; }
        .download-item:hover { border-color: var(--primary); transform: translateX(5px); }
        .download-status { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
        .status-pending { background: rgba(255, 170, 0, 0.2); color: var(--warning); }
        .status-resolving { background: rgba(189, 147, 249, 0.2); color: var(--tube5s-accent); animation: spin 1s linear infinite; }
        .status-downloading { background: rgba(37, 244, 238, 0.2); color: var(--secondary); }
        .status-success { background: rgba(0, 255, 136, 0.2); color: var(--success); }
        .status-error { background: rgba(255, 51, 102, 0.2); color: var(--error); }
        @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        .download-info { flex: 1; }
        .download-url { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 5px; word-break: break-all; }
        .file-name { color: var(--primary); font-weight: 600; margin-top: 5px; }
        .important-notice { color: var(--error); border: 1px solid var(--error); background: rgba(255, 51, 102, 0.1); padding: 20px; border-radius: 12px; margin-top: 20px; }
        .important-notice strong { font-weight: bold; color: var(--warning); }
        .notification { position: fixed; bottom: 20px; right: 20px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 15px 20px; display: flex; align-items: center; gap: 15px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); transform: translateX(400px); transition: transform 0.3s; z-index: 1000; }
        .notification.show { transform: translateX(0); }
    </style>
</head>
<body>
<header>
    <div class="header-content">
        <div class="logo">
            <div class="logo-icon"><i class="fas fa-download"></i></div>
            <h1>Universal Downloader</h1>
        </div>
    </div>
</header>

<div class="container">
    <div class="card">
        <div class="card-header">
            <div class="card-icon"><i class="fas fa-link"></i></div>
            <div class="card-title">Adicionar Links</div>
        </div>

        <div class="input-section">
            <label class="input-label">Cole os links (Tube5s ou Douyin):</label>
            <textarea id="linksInput" class="links-textarea" placeholder="Cole aqui&#10;Links https://https//svX.tube5s.com... OU&#10;Links do Douyin"></textarea>
            <div id="urlChips" class="url-chips"></div>
        </div>

        <div class="action-buttons">
            <button class="btn btn-primary" id="startBtn"><i class="fas fa-play"></i> Processar Links</button>
            <button class="btn btn-secondary" id="clearBtn"><i class="fas fa-trash"></i> Limpar</button>
        </div>

        <div class="progress-section" id="progressSection">
            <div class="progress-header">
                <div class="progress-title">Progresso</div>
                <div style="display: flex; gap: 15px; font-size: 0.9rem;">
                    <span style="color: var(--success)"><span id="successCount">0</span></span>
                    <span style="color: var(--secondary)"><span id="processingCount">0</span></span>
                    <span style="color: var(--error)"><span id="errorCount">0</span></span>
                </div>
            </div>
            <div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
            <div class="downloads-list" id="downloadsList"></div>
        </div>

        <div class="important-notice">
            <h3><i class="fas fa-exclamation-triangle"></i> ATENÇÃO: Para Funcionar</h3>
            <p><strong>NÃO clique duas vezes no arquivo para abrir.</strong> O navegador vai bloquear o download. Você precisa de um "servidor local". Use um dos métodos abaixo para abrir a página corretamente.</p>
            <ol>
                <li><strong>Método Python (Recomendado):</strong> Abra o terminal/prompt na pasta do arquivo e digite: <code>python -m http.server</code>. Depois acesse <a href="http://localhost:8000" target="_blank" style="color: var(--primary);">http://localhost:8000</a>.</li>
                <li><strong>Método VS Code (Mais fácil visualmente):</strong> Abra a pasta no VS Code, instale a extensão "Live Server", clique com o direito no arquivo e escolha "Open with Live Server".</li>
            </ol>
        </div>
    </div>
</div>

<div class="notification" id="notification"><div id="notificationText" style="font-weight: 600;"></div></div>

<script>
document.addEventListener('DOMContentLoaded', () => {
    let downloadQueue = []; let processedCount = 0; let successCount = 0; let processingCount = 0; let errorCount = 0;
    const linksInput = document.getElementById('linksInput'); const urlChips = document.getElementById('urlChips');
    const startBtn = document.getElementById('startBtn'); const clearBtn = document.getElementById('clearBtn');
    const progressSection = document.getElementById('progressSection'); const downloadsList = document.getElementById('downloadsList');
    const progressFill = document.getElementById('progressFill');
    const notification = document.getElementById('notification'); const notificationText = document.getElementById('notificationText');

    function getLinkType(url) { if (url.includes('tube5s.com') || url.includes('sv5.') || url.includes('v3.')) return 'tube5s'; if (url.includes('douyin.com')) return 'douyin'; return 'unknown'; }
    function showNotification(msg, type) { notificationText.textContent = msg; notification.style.border = `1px solid var(--${type})`; notification.style.background = `rgba(${type === 'error' ? '255, 51, 102' : '0, 255, 136'}, 0.1)`; notification.style.color = `var(--${type})`; notification.classList.add('show'); setTimeout(() => notification.classList.remove('show'), 4000); }
    function setStatus(index, status, text) {
        const el = document.getElementById(`status-${index}`); const filename = document.getElementById(`filename-${index}`);
        if (!el) return; if (filename) filename.textContent = text;
        el.classList.remove('status-pending', 'status-resolving', 'status-downloading', 'status-success', 'status-error');
        el.innerHTML = status === 'resolving' ? '<i class="fas fa-bolt"></i>' : status === 'downloading' ? '<i class="fas fa-download"></i>' : status === 'success' ? '<i class="fas fa-check"></i>' : '<i class="fas fa-times"></i>';
        el.className = `download-status status-${status}`;
    }
    async function resolveTube5S(originalUrl) {
        try {
            let cleanUrl = originalUrl.replace('https://https//', 'https://').replace('http://https//', 'https://');
            const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
            const response = await fetch(cleanUrl, { method: 'GET', redirect: 'follow', headers: { 'User-Agent': userAgent } });
            if (response.type === 'opaque') throw new Error("ERRO DE CORS/SERVIDOR: Abra a página via 'python -m http.server' ou 'Live Server' no VS Code, não clicando duas vezes no arquivo.");
            if (!response.ok) throw new Error(`Erro do servidor: ${response.status}`);
            const finalUrl = response.url;
            if (finalUrl === cleanUrl) throw new Error('Redirecionamento falhou. Link inválido?');
            return { success: true, downloadUrl: finalUrl, quality: 'Native (V3)', size: 0, type: 'tube5s' };
        } catch (error) { console.error(error); return { success: false, error: error.message }; }
    }
    async function resolveDouyin(url) { /* Simplificada */ try { const response = await fetch('https://snapdouyin.app/wp-json/mx-downloader/video-data/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `url=${encodeURIComponent(url)}&hash=...&t=${Date.now()}` }); const data = await response.json(); if(data.medias && data.medias.length > 0) { const best = data.medias.sort((a,b)=> (b.size||0)-(a.size||0))[0]; return { success: true, downloadUrl: best.url, quality: best.formattedSize || 'HD', type: 'douyin' }; } throw new Error("API do Douyin não encontrou vídeo."); } catch (e) { return { success: false, error: 'Falha na API do Douyin. ' + e.message }; } }

    function parseLinks() { const urls = linksInput.value.split('\n').map(l => l.trim()).filter(l => l); urlChips.innerHTML = ''; urls.forEach((url, index) => { const type = getLinkType(url); const chip = document.createElement('div'); chip.className = `url-chip ${type === 'tube5s' ? 'type-tube5s' : ''}`; chip.innerHTML = `${type === 'tube5s' ? '<i class="fas fa-bolt"></i>' : '<i class="fab fa-tiktok"></i>'}<span>${url.substring(0, 30)}...</span><i class="fas fa-times remove" onclick="removeLine(${index})"></i>`; urlChips.appendChild(chip); }); return urls; }
    window.removeLine = function(index) { const lines = linksInput.value.split('\n'); lines.splice(index,1); linksInput.value = lines.join('\n'); parseLinks(); };
    function clearLinks() { linksInput.value = ''; urlChips.innerHTML = ''; downloadsList.innerHTML = ''; progressSection.style.display = 'none'; processedCount = 0; successCount = 0; errorCount = 0; processingCount = 0; updateStats(); }
    function updateStats() { document.getElementById('successCount').innerText = successCount; document.getElementById('processingCount').innerText = processingCount; document.getElementById('errorCount').innerText = errorCount; const progress = downloadQueue.length === 0 ? 0 : (processedCount / downloadQueue.length) * 100; progressFill.style.width = `${progress}%`; }
    function createItem(url, index) { const div = document.createElement('div'); div.className = 'download-item'; div.innerHTML = `<div class="download-status status-pending" id="status-${index}"><i class="fas fa-clock"></i></div><div class="download-info"><div class="download-url">${url}</div><div class="file-name" id="filename-${index}">Aguardando...</div><div>Detectado: ${getLinkType(url).toUpperCase()}</div></div>`; return div; }
    function downloadFile(url, filename) { const a = document.createElement('a'); a.href = url; a.download = filename; a.target = '_blank'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }

    async function processQueue() {
        const urls = parseLinks(); if (urls.length === 0) { showNotification('Cole pelo menos um link!', 'error'); return; }
        downloadQueue = urls; processedCount = 0; successCount = 0; errorCount = 0; processingCount = 0;
        downloadsList.innerHTML = ''; progressSection.style.display = 'block'; startBtn.disabled = true;
        urls.forEach((url, i) => downloadsList.appendChild(createItem(url, i)));
        for (let i = 0; i < urls.length; i++) {
            const url = urls[i]; const type = getLinkType(url); processingCount++; updateStats();
            try {
                let result;
                if (type === 'tube5s') { setStatus(i, 'resolving', 'Resolvendo redirect...'); result = await resolveTube5S(url); }
                else { setStatus(i, 'resolving', 'Processando API Douyin...'); result = await resolveDouyin(url); }
                if (result.success) {
                    setStatus(i, 'downloading', 'Baixando...');
                    downloadFile(result.downloadUrl, `${type}_${i}.mp4`);
                    setStatus(i, 'success', `Concluído!`);
                    successCount++;
                } else { throw new Error(result.error); }
            } catch (err) { setStatus(i, 'error', `Falha: ${err.message}`); showNotification(`Falha em ${type}: ${err.message}`, 'error'); errorCount++; }
            processingCount--; processedCount++; updateStats();
        }
        startBtn.disabled = false; showNotification('Processamento concluído!', 'success');
    }
    startBtn.addEventListener('click', processQueue);
    clearBtn.addEventListener('click', clearLinks);
    linksInput.addEventListener('input', parseLinks);
    parseLinks();
});
</script>
</body>
</html>