vod / index.html
hannabaker's picture
Update index.html
46e5393 verified
<!DOCTYPE html>
<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>