diff --git a/app.py b/app.py index 7ad9fe2..530f23f 100644 --- a/app.py +++ b/app.py @@ -67,6 +67,7 @@ def _random_ua(): # --------------------------------------------------------------------------- def _base_opts(extra=None): opts = { + "quiet": True, "cachedir": CACHE_DIR, "js_runtimes": {"node": {"executable": "/usr/bin/node"}}, "http_headers": {"User-Agent": _random_ua()}, diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..1b85051 --- /dev/null +++ b/static/app.js @@ -0,0 +1,207 @@ +let selectedMediaType = 'video'; +let selectedOutputFormat = 'mp4'; +let pollTimer = null; +let debounceTimer = null; +let currentFetchController = null; + +let availableFormats = { audio: [], video: [] }; + +// --------------------------------------------------------------------------- +// Only fire /info when the input looks like a real supported URL. +// --------------------------------------------------------------------------- +const URL_RE = /^https?:\/\/(www\.)?(youtube\.com\/(watch|shorts|live)|youtu\.be|vimeo\.com|dailymotion\.com|twitch\.tv|soundcloud\.com)/i; + +function looksLikeVideoUrl(str) { + try { new URL(str); } catch { return false; } + return URL_RE.test(str); +} + +// --------------------------------------------------------------------------- +// Format chips +// --------------------------------------------------------------------------- +async function loadFormats() { + try { + const res = await fetch('/formats'); + availableFormats = await res.json(); + } catch { + availableFormats = { + audio: ['mp3','aac','m4a','opus','flac','wav','vorbis','alac'], + video: ['mp4','mkv','webm','mov','avi','flv'], + }; + } + renderFormatChips(); +} + +function setMediaType(type) { + selectedMediaType = type; + document.querySelectorAll('.fmt-btn').forEach(b => b.classList.toggle('active', b.dataset.fmt === type)); + renderFormatChips(); +} + +function renderFormatChips() { + const list = selectedMediaType === 'audio' ? availableFormats.audio : availableFormats.video; + const def = list[0] || (selectedMediaType === 'audio' ? 'mp3' : 'mp4'); + selectedOutputFormat = def; + document.getElementById('format-chips').innerHTML = list.map(f => + `` + ).join(''); +} + +function selectFormat(el, f) { + document.querySelectorAll('#format-chips .chip').forEach(c => c.classList.remove('selected')); + el.classList.add('selected'); + selectedOutputFormat = f; +} + +// --------------------------------------------------------------------------- +// UI helpers +// --------------------------------------------------------------------------- +function fmtDuration(s) { + if (!s) return ''; + const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; + return h + ? `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}` + : `${m}:${String(sec).padStart(2,'0')}`; +} + +function showStatus(msg, type) { + const el = document.getElementById('status-msg'); + el.textContent = msg; el.className = type; +} + +function clearStatus() { + const el = document.getElementById('status-msg'); + el.className = ''; el.textContent = ''; +} + +// --------------------------------------------------------------------------- +// Auto-fetch video info (debounced, abortable, URL-guarded) +// --------------------------------------------------------------------------- +async function fetchInfo() { + const url = document.getElementById('url-input').value.trim(); + + if (!looksLikeVideoUrl(url)) { + document.getElementById('info-box').classList.remove('visible'); + clearStatus(); + return; + } + + if (currentFetchController) currentFetchController.abort(); + currentFetchController = new AbortController(); + + clearStatus(); + document.getElementById('info-box').classList.remove('visible'); + document.getElementById('url-spinner').classList.add('active'); + + try { + const res = await fetch('/info', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }), + signal: currentFetchController.signal, + }); + const data = await res.json(); + + if (data.error) { showStatus('⚠ ' + data.error, 'error'); return; } + + document.getElementById('thumb').src = data.thumbnail || ''; + document.getElementById('vid-title').textContent = data.title || 'Unknown'; + document.getElementById('vid-meta').textContent = + [data.uploader, fmtDuration(data.duration)].filter(Boolean).join(' · '); + document.getElementById('info-box').classList.add('visible'); + + } catch (e) { + if (e.name !== 'AbortError') showStatus('⚠ Could not reach server.', 'error'); + } finally { + document.getElementById('url-spinner').classList.remove('active'); + } +} + +// --------------------------------------------------------------------------- +// Download +// --------------------------------------------------------------------------- +async function startDownload() { + const url = document.getElementById('url-input').value.trim(); + if (!url) { showStatus('⚠ Please enter a URL.', 'error'); return; } + clearStatus(); + + if (currentFetchController) { currentFetchController.abort(); currentFetchController = null; } + document.getElementById('url-spinner').classList.remove('active'); + + const btn = document.getElementById('start-btn'); + btn.disabled = true; + btn.innerHTML = ' Starting...'; + + document.getElementById('download-btn-wrap').style.display = 'none'; + document.getElementById('progress-box').classList.add('visible'); + document.getElementById('progress-label').textContent = 'Downloading...'; + document.getElementById('progress-fill').style.width = '0%'; + document.getElementById('prog-pct').textContent = '0%'; + document.getElementById('prog-speed').textContent = ''; + + try { + const res = await fetch('/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, format: selectedMediaType, chosen_format: selectedOutputFormat }), + }); + const data = await res.json(); + if (data.error) { showStatus('⚠ ' + data.error, 'error'); resetBtn(); return; } + pollStatus(data.job_id); + } catch { + showStatus('⚠ Server error.', 'error'); + resetBtn(); + } +} + +function pollStatus(jobId) { + if (pollTimer) clearInterval(pollTimer); + pollTimer = setInterval(async () => { + try { + const data = await (await fetch('/status/' + jobId)).json(); + + if (data.progress !== undefined) { + document.getElementById('progress-fill').style.width = data.progress + '%'; + document.getElementById('prog-pct').textContent = data.progress + '%'; + if (data.speed) document.getElementById('prog-speed').textContent = + data.speed + (data.eta ? ' · ETA ' + data.eta : ''); + } + + if (data.status === 'done') { + clearInterval(pollTimer); + document.getElementById('progress-label').textContent = 'Complete!'; + document.getElementById('prog-speed').textContent = ''; + showStatus('✓ Download ready: ' + (data.title || 'file'), 'success'); + document.getElementById('dl-link').href = '/file/' + jobId; + document.getElementById('dl-link').download = true; + document.getElementById('download-btn-wrap').style.display = 'flex'; + resetBtn(); + } else if (data.status === 'error') { + clearInterval(pollTimer); + showStatus('⚠ ' + (data.error || 'Unknown error'), 'error'); + document.getElementById('progress-box').classList.remove('visible'); + resetBtn(); + } + } catch { /* ignore transient poll errors */ } + }, 800); +} + +function resetBtn() { + const btn = document.getElementById('start-btn'); + btn.disabled = false; btn.innerHTML = 'Download'; +} + +// --------------------------------------------------------------------------- +// Event listeners +// --------------------------------------------------------------------------- +document.getElementById('url-input').addEventListener('input', () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(fetchInfo, 1200); +}); + +document.getElementById('url-input').addEventListener('paste', () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(fetchInfo, 400); +}); + +loadFormats(); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..cc12814 --- /dev/null +++ b/static/style.css @@ -0,0 +1,295 @@ +:root { + --bg: #0a0a0a; + --surface: #111111; + --border: #222222; + --accent: #e8ff47; + --accent2: #ff6b35; + --text: #f0f0f0; + --muted: #666666; + --radius: 4px; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + background: var(--bg); + color: var(--text); + font-family: 'Syne', sans-serif; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + padding: 60px 20px 80px; + background-image: + radial-gradient(ellipse 80% 50% at 50% -10%, rgba(232,255,71,0.07) 0%, transparent 70%), + repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(255,255,255,0.02) 39px, rgba(255,255,255,0.02) 40px), + repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(255,255,255,0.02) 39px, rgba(255,255,255,0.02) 40px); +} + +header { + text-align: center; + margin-bottom: 56px; + animation: fadeDown 0.6s ease both; +} + +.logo { + font-size: clamp(3rem, 8vw, 5.5rem); + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1; + background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.tagline { + font-family: 'Space Mono', monospace; + color: var(--muted); + font-size: 0.75rem; + letter-spacing: 0.2em; + text-transform: uppercase; + margin-top: 8px; +} + +.card { + width: 100%; + max-width: 640px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 36px; + animation: fadeUp 0.5s ease 0.1s both; +} + +.section-label { + font-family: 'Space Mono', monospace; + font-size: 0.65rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 10px; +} + +.url-row { + display: flex; + gap: 10px; + margin-bottom: 28px; + position: relative; +} + +input[type="text"] { + flex: 1; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-family: 'Space Mono', monospace; + font-size: 0.82rem; + padding: 12px 42px 12px 14px; + outline: none; + transition: border-color 0.2s; +} +input[type="text"]:focus { border-color: var(--accent); } +input[type="text"]::placeholder { color: var(--muted); } + +.url-spinner { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + width: 14px; + height: 14px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.6s linear infinite; + display: none; +} +.url-spinner.active { display: block; } + +.btn { + font-family: 'Syne', sans-serif; + font-weight: 700; + font-size: 0.85rem; + letter-spacing: 0.05em; + padding: 12px 20px; + border: none; + border-radius: var(--radius); + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} +.btn-accent { background: var(--accent); color: #000; } +.btn-accent:hover { background: #d4eb3a; transform: translateY(-1px); } +.btn-accent:active { transform: translateY(0); } +.btn-accent:disabled { opacity: 0.4; cursor: not-allowed; transform: none; } + +#info-box { + display: none; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; + margin-bottom: 24px; + gap: 14px; + animation: fadeUp 0.3s ease both; +} +#info-box.visible { display: flex; } + +.thumb-wrap { + flex-shrink: 0; + width: 100px; + height: 60px; + border-radius: 4px; + overflow: hidden; + background: var(--border); +} +.thumb-wrap img { width: 100%; height: 100%; object-fit: cover; } + +.info-meta { flex: 1; min-width: 0; } + +.info-title { + font-weight: 700; + font-size: 0.9rem; + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 6px; +} + +.info-sub { + font-family: 'Space Mono', monospace; + font-size: 0.7rem; + color: var(--muted); +} + +.format-toggle { + display: flex; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + margin-bottom: 20px; +} + +.fmt-btn { + flex: 1; + padding: 11px; + text-align: center; + cursor: pointer; + font-family: 'Syne', sans-serif; + font-weight: 600; + font-size: 0.82rem; + border: none; + background: transparent; + color: var(--muted); + transition: all 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 7px; +} +.fmt-btn.active { background: var(--accent); color: #000; } +.fmt-btn:not(.active):hover { color: var(--text); } + +#format-row { margin-bottom: 28px; } + +.format-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.chip { + font-family: 'Space Mono', monospace; + font-size: 0.72rem; + padding: 6px 14px; + border: 1px solid var(--border); + border-radius: 20px; + cursor: pointer; + color: var(--muted); + transition: all 0.15s; + background: transparent; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.chip.selected { border-color: var(--accent); color: var(--accent); background: rgba(232,255,71,0.07); } +.chip:hover:not(.selected) { border-color: #444; color: var(--text); } + +#progress-box { display: none; margin-bottom: 24px; } +#progress-box.visible { display: block; } + +.progress-bar-bg { + height: 6px; + background: var(--border); + border-radius: 3px; + overflow: hidden; + margin: 10px 0 8px; +} + +.progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--accent2)); + border-radius: 3px; + width: 0%; + transition: width 0.4s ease; +} + +.progress-meta { + font-family: 'Space Mono', monospace; + font-size: 0.68rem; + color: var(--muted); + display: flex; + justify-content: space-between; +} + +#status-msg { + font-family: 'Space Mono', monospace; + font-size: 0.72rem; + padding: 10px 14px; + border-radius: var(--radius); + margin-bottom: 20px; + display: none; +} +#status-msg.error { + background: rgba(255,80,80,0.1); + border: 1px solid rgba(255,80,80,0.3); + color: #ff8080; + display: block; +} +#status-msg.success { + background: rgba(232,255,71,0.08); + border: 1px solid rgba(232,255,71,0.3); + color: var(--accent); + display: block; +} + +.action-row { display: flex; gap: 10px; justify-content: flex-end; } +#download-btn-wrap { display: none; } + +.spin { + display: inline-block; + width: 12px; height: 12px; + border: 2px solid rgba(0,0,0,0.3); + border-top-color: #000; + border-radius: 50%; + animation: spin 0.6s linear infinite; + vertical-align: middle; +} + +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes fadeDown { from { opacity:0; transform:translateY(-20px); } to { opacity:1; transform:translateY(0); } } +@keyframes fadeUp { from { opacity:0; transform:translateY(16px); } to { opacity:1; transform:translateY(0); } } + +footer { + margin-top: 48px; + font-family: 'Space Mono', monospace; + font-size: 0.65rem; + color: #333; + letter-spacing: 0.1em; + text-align: center; + animation: fadeUp 0.5s ease 0.3s both; +} diff --git a/templates/index.html b/templates/index.html index cad296e..788ef83 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,308 +3,10 @@ -VDROP — Video Downloader +VDROP - + @@ -315,7 +17,7 @@
-
Paste URL
+
Ingresar URL
@@ -331,7 +33,7 @@
- +
- +
- +
@@ -363,232 +65,13 @@ - +
- +