yt-dl/index.html

307 lines
8.2 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YT → MP3</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0d0d;
--surface: #141414;
--border: #2a2a2a;
--accent: #e8ff47;
--accent-dim: #b8cc2a;
--text: #e8e8e8;
--muted: #555;
--error: #ff4747;
--success: #47ff8a;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Space Mono', monospace;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background-image:
repeating-linear-gradient(0deg, transparent, transparent 39px, #1a1a1a 39px, #1a1a1a 40px),
repeating-linear-gradient(90deg, transparent, transparent 39px, #1a1a1a 39px, #1a1a1a 40px);
}
.card {
width: 100%;
max-width: 560px;
background: var(--surface);
border: 1px solid var(--border);
padding: 0;
position: relative;
}
.card::before {
content: '';
position: absolute;
top: -3px; left: -3px;
right: -3px; bottom: -3px;
border: 1px solid var(--accent);
pointer-events: none;
opacity: 0.15;
}
.header {
padding: 2rem 2rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: baseline;
gap: 1rem;
}
.header h1 {
font-family: 'Bebas Neue', sans-serif;
font-size: 3.5rem;
letter-spacing: 0.05em;
color: var(--accent);
line-height: 1;
}
.header .sub {
font-size: 0.7rem;
color: var(--muted);
letter-spacing: 0.15em;
text-transform: uppercase;
}
.form-body {
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
label {
font-size: 0.65rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
input {
background: #0d0d0d;
border: 1px solid var(--border);
color: var(--text);
font-family: 'Space Mono', monospace;
font-size: 0.85rem;
padding: 0.75rem 1rem;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
width: 100%;
}
input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent);
}
input::placeholder {
color: #333;
}
.divider {
height: 1px;
background: var(--border);
margin: 0.25rem 0;
}
button[type="submit"] {
background: var(--accent);
color: #0d0d0d;
border: none;
font-family: 'Bebas Neue', sans-serif;
font-size: 1.4rem;
letter-spacing: 0.12em;
padding: 1rem;
cursor: pointer;
width: 100%;
transition: background 0.15s, transform 0.1s;
position: relative;
overflow: hidden;
}
button[type="submit"]:hover { background: #d4eb30; }
button[type="submit"]:active { transform: scale(0.99); }
button[type="submit"]:disabled {
background: #3a3a1a;
color: #666;
cursor: not-allowed;
}
.status-box {
margin: 0 2rem 2rem;
padding: 1rem;
border: 1px solid var(--border);
font-size: 0.78rem;
display: none;
align-items: center;
gap: 0.75rem;
line-height: 1.5;
}
.status-box.show { display: flex; }
.status-box.running { border-color: var(--accent); color: var(--accent); }
.status-box.done { border-color: var(--success); color: var(--success); }
.status-box.error { border-color: var(--error); color: var(--error); }
.spinner {
width: 18px; height: 18px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
flex-shrink: 0;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.icon { font-size: 1.2rem; flex-shrink: 0; }
.footer-bar {
padding: 0.75rem 2rem;
border-top: 1px solid var(--border);
font-size: 0.6rem;
color: var(--muted);
letter-spacing: 0.1em;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>YT→MP3</h1>
<span class="sub">YouTube Downloader</span>
</div>
<div class="form-body">
<div class="field">
<label>URL</label>
<input type="url" id="url" placeholder="https://youtube.com/watch?v=..." autocomplete="off"/>
</div>
<div class="divider"></div>
<div class="field">
<label>Interpret</label>
<input type="text" id="artist" placeholder="z.B. Die Ärzte"/>
</div>
<div class="field">
<label>Titel</label>
<input type="text" id="title" placeholder="z.B. Männer sind Schweine"/>
</div>
<div class="field">
<label>Album</label>
<input type="text" id="album" placeholder="z.B. 13"/>
</div>
<button type="submit" id="btn" onclick="startDownload()">
DOWNLOAD STARTEN
</button>
</div>
<div class="status-box" id="status-box">
<span id="status-icon" class="icon"></span>
<span id="status-msg"></span>
</div>
<div class="footer-bar">Downloads → ./downloads/</div>
</div>
<script>
let pollInterval = null;
async function startDownload() {
const url = document.getElementById('url').value.trim();
const artist = document.getElementById('artist').value.trim();
const title = document.getElementById('title').value.trim();
const album = document.getElementById('album').value.trim();
if (!url) { showStatus('error', '⚠', 'Bitte eine URL eingeben.'); return; }
const btn = document.getElementById('btn');
btn.disabled = true;
showStatus('running', null, 'Verbinde mit YouTube...');
try {
const res = await fetch('/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, artist, title, album })
});
const data = await res.json();
if (data.error) { showStatus('error', '✕', data.error); btn.disabled = false; return; }
pollStatus(data.task_id);
} catch (e) {
showStatus('error', '✕', 'Verbindungsfehler: ' + e.message);
btn.disabled = false;
}
}
function pollStatus(taskId) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const res = await fetch('/status/' + taskId);
const data = await res.json();
if (data.status === 'running') {
showStatus('running', null, data.message);
} else if (data.status === 'done') {
clearInterval(pollInterval);
showStatus('done', '✓', data.message);
document.getElementById('btn').disabled = false;
} else if (data.status === 'error') {
clearInterval(pollInterval);
showStatus('error', '✕', data.message);
document.getElementById('btn').disabled = false;
}
} catch (e) {
clearInterval(pollInterval);
showStatus('error', '✕', 'Polling-Fehler');
document.getElementById('btn').disabled = false;
}
}, 1500);
}
function showStatus(type, icon, msg) {
const box = document.getElementById('status-box');
const iconEl = document.getElementById('status-icon');
const msgEl = document.getElementById('status-msg');
box.className = 'status-box show ' + type;
msgEl.textContent = msg;
if (type === 'running') {
iconEl.innerHTML = '<div class="spinner"></div>';
iconEl.className = 'icon';
} else {
iconEl.textContent = icon || '';
iconEl.className = 'icon';
}
}
</script>
</body>
</html>