Primo Caricamento

This commit is contained in:
2026-01-10 13:21:36 +01:00
commit a36973ae12
10 changed files with 630 additions and 0 deletions

68
frontend/index.html Normal file
View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YT Downloader</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<header>
<h1>YT Downloader</h1>
<p>Inserisci il link, scegli video o solo audio, poi scarica.</p>
</header>
<section class="card">
<label for="url">Link YouTube</label>
<div class="inline">
<input id="url" type="url" placeholder="https://www.youtube.com/watch?v=..." />
<button id="fetchInfo">Carica formati</button>
</div>
<div class="options">
<div>
<label for="mode">Tipo</label>
<select id="mode">
<option value="video">Video</option>
<option value="audio">Solo audio</option>
</select>
</div>
<div>
<label for="videoFormat">Risoluzione video</label>
<select id="videoFormat" disabled>
<option value="">Best disponibile</option>
</select>
</div>
<div>
<label for="audioFormat">Formato audio</label>
<select id="audioFormat" disabled>
<option value="">Best disponibile</option>
</select>
</div>
<div>
<label for="audioExt">Estensione audio</label>
<select id="audioExt" disabled>
<option value="mp3">mp3</option>
<option value="m4a">m4a</option>
<option value="opus">opus</option>
<option value="aac">aac</option>
</select>
</div>
</div>
<div class="actions">
<button id="download">Scarica</button>
<span id="status"></span>
</div>
<div id="result" class="result hidden"></div>
</section>
</main>
<script src="main.js"></script>
</body>
</html>

118
frontend/main.js Normal file
View File

@@ -0,0 +1,118 @@
const urlInput = document.getElementById('url');
const fetchInfoBtn = document.getElementById('fetchInfo');
const downloadBtn = document.getElementById('download');
const statusEl = document.getElementById('status');
const resultEl = document.getElementById('result');
const modeSelect = document.getElementById('mode');
const videoSelect = document.getElementById('videoFormat');
const audioSelect = document.getElementById('audioFormat');
const audioExt = document.getElementById('audioExt');
let lastFormats = { video: [], audio: [] };
function setStatus(text) {
statusEl.textContent = text || '';
}
function showResult(html) {
if (!html) {
resultEl.classList.add('hidden');
resultEl.innerHTML = '';
return;
}
resultEl.classList.remove('hidden');
resultEl.innerHTML = html;
}
function fillSelect(select, items, formatter) {
select.innerHTML = '<option value="">Best disponibile</option>';
items.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id;
opt.textContent = formatter(item);
select.appendChild(opt);
});
select.disabled = items.length === 0;
}
async function fetchInfo() {
const url = urlInput.value.trim();
if (!url) {
showResult('Inserisci un URL.');
return;
}
setStatus('Recupero formati...');
showResult('');
try {
const res = await fetch('/api/info', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Errore');
lastFormats.video = data.video_formats || [];
lastFormats.audio = data.audio_formats || [];
fillSelect(videoSelect, lastFormats.video, (f) => {
const reso = f.resolution ? `${f.resolution}` : 'Auto';
const fps = f.fps ? `${f.fps}fps` : '';
const size = f.size ? ` · ${f.size}` : '';
return `${reso} ${fps}${size} (${f.ext})`;
});
fillSelect(audioSelect, lastFormats.audio, (f) => {
const abr = f.abr ? `${f.abr}kbps` : 'bitrate auto';
const size = f.size ? ` · ${f.size}` : '';
return `${f.ext} ${abr}${size}`;
});
audioExt.disabled = false;
const minutes = data.duration ? Math.round(data.duration / 60) : '?';
showResult(`Titolo: <strong>${data.title || 'sconosciuto'}</strong><br/>Durata: ${minutes} min`);
setStatus('Pronto al download');
} catch (err) {
showResult(`Errore: ${err.message}`);
setStatus('');
}
}
async function startDownload() {
const url = urlInput.value.trim();
if (!url) {
showResult('Inserisci un URL.');
return;
}
setStatus('Scarico...');
showResult('');
const mode = modeSelect.value;
const payload = {
url,
mode,
format_id: mode === 'video' ? videoSelect.value : audioSelect.value,
audio_ext: mode === 'audio' ? audioExt.value : undefined,
};
try {
const res = await fetch('/api/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Errore');
const link = `<a href="${data.url}" download>${data.file}</a>`;
showResult(`File pronto: ${link}`);
setStatus('Completo');
} catch (err) {
showResult(`Errore: ${err.message}`);
setStatus('');
}
}
fetchInfoBtn.addEventListener('click', fetchInfo);
downloadBtn.addEventListener('click', startDownload);

118
frontend/style.css Normal file
View File

@@ -0,0 +1,118 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
background: radial-gradient(circle at 20% 20%, #f4f7ff, #e8ecf7 25%, #f8fbff 50%);
color: #0d1b2a;
}
.shell {
max-width: 880px;
margin: 48px auto;
padding: 0 20px;
}
header h1 {
margin: 0;
font-size: 32px;
}
header p {
margin: 6px 0 18px;
color: #4a6072;
}
.card {
background: #fff;
border: 1px solid #e4e9f2;
border-radius: 16px;
padding: 20px;
box-shadow: 0 12px 32px rgba(13, 27, 42, 0.06);
}
label {
display: block;
font-weight: 600;
margin-bottom: 6px;
}
input, select {
width: 100%;
padding: 12px;
border: 1px solid #cdd7e0;
border-radius: 10px;
font-size: 15px;
}
.inline {
display: grid;
grid-template-columns: 1fr 140px;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
button {
padding: 12px 16px;
border: none;
border-radius: 12px;
background: linear-gradient(120deg, #0e7afe, #5ad4f7);
color: #fff;
font-weight: 700;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.2s ease;
}
button:active {
transform: translateY(1px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
}
.actions {
margin-top: 16px;
display: flex;
align-items: center;
gap: 12px;
}
#status {
color: #0e7afe;
font-weight: 600;
}
.result {
margin-top: 16px;
padding: 12px;
border-radius: 10px;
background: #f0f6ff;
border: 1px solid #cde1ff;
color: #0d1b2a;
}
.hidden {
display: none;
}
@media (max-width: 640px) {
.inline {
grid-template-columns: 1fr;
}
button {
width: 100%;
}
}