Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>VOD Archiver Pro</title> | |
| <style> | |
| :root { | |
| --bg-primary: #121212; --bg-secondary: #1E1E1E; --bg-tertiary: #2A2A2A; | |
| --accent: #6C63FF; --accent-hover: #574BFF; --success: #1DB954; --error: #F44336; | |
| --text-primary: #FFFFFF; --text-secondary: #B3B3B3; --border: #3A3A3A; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| background: var(--bg-primary); color: var(--text-primary); | |
| display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 1rem; | |
| } | |
| .container { | |
| width: 100%; max-width: 800px; background: var(--bg-secondary); | |
| border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); overflow: hidden; | |
| } | |
| .header { | |
| padding: 2rem; text-align: center; background: var(--bg-tertiary); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .header h1 { font-size: 2rem; font-weight: 700; color: var(--accent); margin-bottom: 0.5rem; } | |
| .header p { color: var(--text-secondary); } | |
| .content { padding: 2rem; } | |
| .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem; } | |
| .input-group { display: flex; flex-direction: column; } | |
| .input-group.full-width { grid-column: 1 / -1; } | |
| label { margin-bottom: 0.5rem; font-weight: 500; color: var(--text-secondary); } | |
| input, select { | |
| padding: 0.75rem; background: var(--bg-primary); border: 1px solid var(--border); | |
| border-radius: 8px; color: var(--text-primary); font-size: 1rem; transition: all 0.2s; | |
| } | |
| input:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); } | |
| .button-group { display: flex; gap: 1rem; margin-top: 1.5rem; } | |
| button { | |
| flex-grow: 1; padding: 0.8rem; font-size: 1rem; font-weight: 600; border: none; | |
| border-radius: 8px; cursor: pointer; transition: all 0.2s; | |
| } | |
| .btn-primary { background: var(--accent); color: white; } | |
| .btn-primary:hover:not(:disabled) { background: var(--accent-hover); } | |
| .btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); } | |
| .btn-secondary:hover:not(:disabled) { background: #333; } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .status-section { display: none; margin-top: 2rem; } | |
| .progress-container { margin-bottom: 1rem; display: none; } | |
| .progress-info { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.9rem; } | |
| .progress-bar { width: 100%; height: 8px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; } | |
| .progress-fill { height: 100%; background: var(--accent); width: 0%; transition: width 0.3s ease; } | |
| .log-container { | |
| background: var(--bg-primary); border-radius: 8px; padding: 1rem; height: 200px; | |
| overflow-y: auto; font-family: monospace; font-size: 0.9rem; position: relative; margin-top: 1rem; | |
| } | |
| .log-controls { position: absolute; top: 0.5rem; right: 0.5rem; display: flex; gap: 0.5rem; } | |
| .log-btn { background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.5rem; cursor: pointer; } | |
| .log-btn.active { background: var(--accent); color: white; } | |
| .log-entry.error { color: var(--error); } .log-entry.success { color: var(--success); } | |
| .video-controls { display: none; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--border); } | |
| .video-controls h3 { margin-bottom: 1rem; } | |
| video { width: 100%; border-radius: 8px; background: black; } | |
| .video-button-group { display: flex; gap: 1rem; margin-top: 1rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"><h1>VOD Archiver Pro</h1><p>Download, Preview, and Archive Twitch VODs</p></div> | |
| <div class="content"> | |
| <form id="mainForm"> | |
| <div class="form-grid"> | |
| <div class="input-group full-width"> | |
| <label for="vodUrl">Twitch VOD URL</label> | |
| <input type="text" id="vodUrl" placeholder="https://www.twitch.tv/videos/..." required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="megaUser">Mega.nz Email</label> | |
| <input type="email" id="megaUser" required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="megaPass">Mega.nz Password</label> | |
| <input type="password" id="megaPass" required> | |
| </div> | |
| <div class="input-group full-width"> | |
| <label for="formatSelect">Quality</label> | |
| <select id="formatSelect" disabled><option>Click "Get Qualities" first</option></select> | |
| </div> | |
| </div> | |
| <div class="button-group"> | |
| <button type="button" class="btn-secondary" id="qualityBtn">Get Qualities</button> | |
| <button type="submit" class="btn-primary" id="startBtn" disabled>Start Archiving</button> | |
| </div> | |
| </form> | |
| <div class="status-section" id="statusSection"> | |
| <div class="progress-container" id="downloadProgress"> | |
| <div class="progress-info"><span>Download</span><span id="downloadInfo"></span></div> | |
| <div class="progress-bar"><div class="progress-fill" id="downloadFill"></div></div> | |
| </div> | |
| <div class="progress-container" id="uploadProgress"> | |
| <div class="progress-info"><span>Upload</span><span id="uploadInfo"></span></div> | |
| <div class="progress-bar"><div class="progress-fill" id="uploadFill"></div></div> | |
| </div> | |
| <div class="log-container" id="logContainer"> | |
| <div class="log-controls"> | |
| <button type="button" class="log-btn" id="clearLogBtn">Clear</button> | |
| <button type="button" class="log-btn active" id="scrollLockBtn">Auto Scroll</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="video-controls" id="videoControls"> | |
| <h3>Preview & Control</h3> | |
| <video id="videoPlayer" controls></video> | |
| <div class="video-button-group"> | |
| <button type="button" class="btn-secondary" id="downloadBtn">Download File</button> | |
| <button type="button" class="btn-secondary" id="copyLinkBtn">Copy Link</button> | |
| <button type="button" class="btn-primary" id="cleanupBtn" style="background: var(--error);">Clean Up Server File</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const el = { | |
| form: document.getElementById('mainForm'), vodUrl: document.getElementById('vodUrl'), | |
| megaUser: document.getElementById('megaUser'), megaPass: document.getElementById('megaPass'), | |
| formatSelect: document.getElementById('formatSelect'), qualityBtn: document.getElementById('qualityBtn'), | |
| startBtn: document.getElementById('startBtn'), statusSection: document.getElementById('statusSection'), | |
| downloadProgress: document.getElementById('downloadProgress'), downloadInfo: document.getElementById('downloadInfo'), | |
| downloadFill: document.getElementById('downloadFill'), uploadProgress: document.getElementById('uploadProgress'), | |
| uploadInfo: document.getElementById('uploadInfo'), uploadFill: document.getElementById('uploadFill'), | |
| logContainer: document.getElementById('logContainer'), clearLogBtn: document.getElementById('clearLogBtn'), | |
| scrollLockBtn: document.getElementById('scrollLockBtn'), videoControls: document.getElementById('videoControls'), | |
| videoPlayer: document.getElementById('videoPlayer'), downloadBtn: document.getElementById('downloadBtn'), | |
| copyLinkBtn: document.getElementById('copyLinkBtn'), cleanupBtn: document.getElementById('cleanupBtn'), | |
| }; | |
| let autoScroll = true; let currentFileId = null; | |
| el.qualityBtn.addEventListener('click', fetchQualities); | |
| el.form.addEventListener('submit', startProcess); | |
| el.scrollLockBtn.addEventListener('click', () => { autoScroll = !autoScroll; el.scrollLockBtn.classList.toggle('active', autoScroll); }); | |
| el.clearLogBtn.addEventListener('click', () => { const controls = el.logContainer.querySelector('.log-controls'); el.logContainer.innerHTML = ''; el.logContainer.appendChild(controls); }); | |
| async function fetchQualities() { | |
| if (!el.vodUrl.value) { alert('Please enter a VOD URL.'); return; } | |
| setLoading(el.qualityBtn, true); | |
| try { | |
| const response = await fetch('/get_formats', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: el.vodUrl.value }) }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| el.formatSelect.innerHTML = ''; | |
| data.formats.forEach(f => { const option = document.createElement('option'); option.value = f.id; option.textContent = f.label; el.formatSelect.appendChild(option); }); | |
| el.formatSelect.disabled = false; el.startBtn.disabled = false; | |
| addLog('Quality options loaded.', 'success'); | |
| } catch (error) { addLog(`Error fetching qualities: ${error.message}`, 'error'); } finally { setLoading(el.qualityBtn, false); } | |
| } | |
| function startProcess(e) { | |
| e.preventDefault(); resetUI(); setProcessing(true); | |
| const payload = { url: el.vodUrl.value, mega_user: el.megaUser.value, mega_pass: el.megaPass.value, format_id: el.formatSelect.value }; | |
| fetch('/process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) | |
| .then(response => { | |
| const reader = response.body.getReader(); const decoder = new TextDecoder(); | |
| function read() { | |
| reader.read().then(({ done, value }) => { | |
| if (done) { handleStreamEnd(); return; } | |
| decoder.decode(value, { stream: true }).split('\n\n').forEach(line => { | |
| if (line.startsWith('data:')) handleSSEMessage(line.substring(5)); | |
| }); | |
| read(); | |
| }); | |
| } | |
| read(); | |
| }).catch(err => { addLog(`Failed to start process: ${err}`, 'error'); setProcessing(false); }); | |
| } | |
| function handleSSEMessage(jsonString) { | |
| try { | |
| const data = JSON.parse(jsonString); | |
| switch (data.type) { | |
| case 'phase': | |
| addLog(data.text); | |
| if (data.phase === 'download') el.downloadProgress.style.display = 'block'; | |
| if (data.phase === 'upload') el.uploadProgress.style.display = 'block'; | |
| break; | |
| case 'progress': updateProgressUI(data.phase, data); break; | |
| case 'file_ready': currentFileId = data.file_id; setupVideoControls(data.file_id); addLog('File is ready for preview and download.', 'success'); break; | |
| case 'complete': addLog(data.text, 'success'); break; | |
| case 'error': addLog(`ERROR: ${data.message}`, 'error'); setProcessing(false); break; | |
| case 'done': setProcessing(false); break; | |
| } | |
| } catch (e) { /* Ignore non-json messages */ } | |
| } | |
| function handleStreamEnd() { addLog("Process stream finished."); setProcessing(false); } | |
| function updateProgressUI(phase, data) { | |
| const fill = phase === 'download' ? el.downloadFill : el.uploadFill; | |
| const info = phase === 'download' ? el.downloadInfo : el.uploadInfo; | |
| fill.style.width = `${data.percent}%`; | |
| let infoText = `${data.percent.toFixed(1)}%`; | |
| if (data.speed) infoText += ` (${data.speed} - ETA: ${data.eta || 'N/A'})`; | |
| info.textContent = infoText; | |
| } | |
| function setupVideoControls(fileId) { | |
| el.videoControls.style.display = 'block'; | |
| el.videoPlayer.src = `/video/${fileId}`; | |
| el.downloadBtn.onclick = () => window.location.href = `/download/${fileId}`; | |
| el.copyLinkBtn.onclick = () => { navigator.clipboard.writeText(`${window.location.origin}/video/${fileId}`); alert('Link copied!'); }; | |
| el.cleanupBtn.onclick = async () => { | |
| if (confirm('Are you sure you want to delete the file from the server?')) { | |
| setLoading(el.cleanupBtn, true); | |
| await fetch(`/cleanup/${fileId}`, { method: 'POST' }); | |
| addLog(`File ${fileId} cleaned up.`, 'success'); | |
| el.videoControls.style.display = 'none'; currentFileId = null; | |
| setLoading(el.cleanupBtn, false); | |
| } | |
| }; | |
| } | |
| function resetUI() { | |
| el.statusSection.style.display = 'block'; el.videoControls.style.display = 'none'; | |
| const controls = el.logContainer.querySelector('.log-controls'); el.logContainer.innerHTML = ''; el.logContainer.appendChild(controls); | |
| currentFileId = null; | |
| ['download', 'upload'].forEach(p => { document.getElementById(`${p}Progress`).style.display = 'none'; document.getElementById(`${p}Fill`).style.width = '0%'; }); | |
| } | |
| function setProcessing(is) { el.startBtn.disabled = is; el.qualityBtn.disabled = is; } | |
| function setLoading(btn, is) { btn.disabled = is; } | |
| </script> | |
| </body> | |
| </html> |