1
0
Fork 0

Fork to yt-mp3-ui

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2026-07-26 08:20:36 +02:00
commit 9c6360e9c4
Signed by: jriou
GPG key ID: 9A099EDA51316854
10 changed files with 243 additions and 288 deletions

View file

@ -9,13 +9,19 @@ const thumbnail = document.getElementById('thumbnail');
const videoTitle = document.getElementById('video-title');
const uploaderEl = document.getElementById('uploader');
const durationEl = document.getElementById('duration');
const formatSection = document.getElementById('format-section');
const formatSelect = document.getElementById('format-select');
const downloadBtn = document.getElementById('download-btn');
const progressWrap = document.getElementById('progress-wrap');
const progressFill = document.getElementById('progress-fill');
const progressLabel = document.getElementById('progress-label');
const cancelBtn = document.getElementById('cancel-btn');
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', () => {
const next =
document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
let currentFileId = null;
let currentAbort = null;
@ -50,7 +56,6 @@ function resetUI() {
clearBtn.style.display = 'none';
errorMsg.style.display = 'none';
preview.style.display = 'none';
formatSection.style.display = 'none';
downloadBtn.style.display = 'none';
progressWrap.style.display = 'none';
thumbnail.src = '';
@ -80,7 +85,7 @@ function setLoading(btn, loading, text) {
btn.textContent = text;
} else if (!loading && btn.dataset.label) {
if (btn.id === 'download-btn') {
btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Download`;
btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Télécharger`;
} else {
btn.textContent = btn.dataset.label;
}
@ -106,32 +111,6 @@ function formatCount(n) {
return n.toString();
}
function formatBytes(bytes) {
if (!bytes) return null;
if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + ' GB';
if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + ' MB';
return (bytes / 1e3).toFixed(0) + ' KB';
}
function formatLabel(f) {
const parts = [];
const res = f.resolution || '';
if (res && res !== 'unknown') parts.push(res);
if (f.ext) parts.push(f.ext.toUpperCase());
const hasVideo = f.vcodec && f.vcodec !== 'none';
const hasAudio = f.acodec && f.acodec !== 'none';
if (!hasVideo && hasAudio) parts.push('audio only');
if (f.fps && hasVideo) parts.push(`${f.fps}fps`);
const size = formatBytes(f.filesize);
if (size) parts.push(`~${size}`);
return parts.join(' · ') || f.format_id;
}
function setProgress(pct, label) {
progressFill.style.width = pct + '%';
progressLabel.innerHTML = label;
@ -139,11 +118,11 @@ function setProgress(pct, label) {
function formatEta(secs) {
if (secs == null || secs < 0) return '';
if (secs < 60) return `ETA ${secs}s`;
if (secs < 60) return `Reste ${secs}s`;
const m = Math.floor(secs / 60);
const s = secs % 60;
return `ETA ${m}m ${s}s`;
return `Reste ${m}m ${s}s`;
}
urlInput.addEventListener('input', () => {
@ -163,16 +142,15 @@ fetchBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) {
showError('Please paste a video URL first.');
showError('Veuillez d\'abord coller une URL de vidéo.');
return;
}
hideError();
preview.style.display = 'none';
formatSection.style.display = 'none';
downloadBtn.style.display = 'none';
progressWrap.style.display = 'none';
setLoading(fetchBtn, true, 'Fetching…');
setLoading(fetchBtn, true, 'Recherche…');
try {
const res = await fetch(`${API}/info`, {
@ -184,16 +162,14 @@ fetchBtn.addEventListener('click', async () => {
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || 'Failed to fetch video info.');
throw new Error(data.detail || 'Échec de la récupération des infos de la vidéo.');
}
renderPreview(data);
renderFormats(data.formats || []);
preview.style.display = 'flex';
formatSection.style.display = 'flex';
downloadBtn.style.display = 'flex';
} catch (err) {
showError(err.message || 'Failed to fetch video info.');
showError(err.message || 'Échec de la récupération des infos de la vidéo.');
} finally {
setLoading(fetchBtn, false);
}
@ -201,10 +177,10 @@ fetchBtn.addEventListener('click', async () => {
function renderPreview(info) {
thumbnail.src = info.thumbnail || '';
thumbnail.alt = info.title || 'Video thumbnail';
thumbnail.alt = info.title || 'Miniature de la vidéo';
thumbnail.style.display = info.thumbnail ? 'block' : 'none';
videoTitle.textContent = info.title || 'Unknown title';
videoTitle.textContent = info.title || 'Titre inconnu';
uploaderEl.textContent = info.uploader || '';
uploaderEl.style.display = info.uploader ? '' : 'none';
@ -227,7 +203,7 @@ function renderPreview(info) {
const d = info.upload_date;
const formatted = new Date(
`${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`
).toLocaleDateString(undefined, {
).toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'short',
day: 'numeric',
@ -240,71 +216,14 @@ function renderPreview(info) {
}
}
function renderFormats(formats) {
formatSelect.innerHTML = '';
const sorted = [...formats].sort(
(a, b) => (b.height || 0) - (a.height || 0) || (b.tbr || 0) - (a.tbr || 0)
);
const combined = sorted.filter((f) => f.vcodec !== 'none' && f.acodec !== 'none');
const videoOnly = sorted.filter((f) => f.vcodec !== 'none' && f.acodec === 'none');
const audioOnly = sorted.filter((f) => f.vcodec === 'none' && f.acodec !== 'none');
const addGroup = (label, items, transformValue = null) => {
if (!items.length) return;
const group = document.createElement('optgroup');
group.label = label;
items.forEach((f) => {
const opt = document.createElement('option');
opt.value = transformValue ? transformValue(f) : f.format_id;
opt.textContent = formatLabel(f);
group.appendChild(opt);
});
formatSelect.appendChild(group);
};
addGroup(
'Video only',
videoOnly,
(f) => `${f.format_id}+bestaudio[ext=m4a]/bestaudio`
);
addGroup('Video + Audio', combined);
addGroup('Audio only', audioOnly);
// Prefer highest MP4 video-only first, because backend can merge audio into MP4
const bestMp4VideoOnly = videoOnly.find((f) => f.ext === 'mp4');
// Fallback to highest combined MP4
const bestMp4Combined = combined.find((f) => f.ext === 'mp4');
// Fallback to any top combined format
const bestCombined = combined[0];
// Final fallback
const bestDefault =
(bestMp4VideoOnly && `${bestMp4VideoOnly.format_id}+bestaudio[ext=m4a]/bestaudio`) ||
(bestMp4Combined && bestMp4Combined.format_id) ||
(bestCombined && bestCombined.format_id) ||
(sorted[0] && sorted[0].format_id);
if (bestDefault) {
formatSelect.value = bestDefault;
}
}
downloadBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) return;
hideError();
setLoading(downloadBtn, true, 'Downloading…');
setLoading(downloadBtn, true, 'Téléchargement…');
progressWrap.style.display = 'block';
setProgress(0, '<span class="meta">Starting…</span>');
setProgress(0, '<span class="meta">Démarrage…</span>');
const fileId = crypto.randomUUID();
currentFileId = fileId;
@ -316,7 +235,7 @@ downloadBtn.addEventListener('click', async () => {
const d = JSON.parse(e.data);
if (d.status === 'starting') {
setProgress(0, '<span class="meta">Starting…</span>');
setProgress(0, '<span class="meta">Démarrage…</span>');
} else if (d.status === 'downloading') {
const pct = d.percent || 0;
const eta = d.eta != null ? formatEta(d.eta) : '';
@ -330,7 +249,7 @@ downloadBtn.addEventListener('click', async () => {
} else if (d.status === 'finished') {
setProgress(
100,
'<span class="pct">100%</span>&ensp;<span class="meta">Processing…</span>'
'<span class="pct">100%</span>&ensp;<span class="meta">Traitement…</span>'
);
} else if (d.status === 'ready') {
sse.close();
@ -340,7 +259,7 @@ downloadBtn.addEventListener('click', async () => {
if (currentFileId === fileId) {
setProgress(
100,
'<span class="pct">✓</span>&ensp;<span class="meta">Done — saving to your downloads</span>'
'<span class="pct">✓</span>&ensp;<span class="meta">Terminé — enregistrement dans vos téléchargements</span>'
);
window.location.href = `${API}/file/${fileId}`;
@ -363,7 +282,7 @@ downloadBtn.addEventListener('click', async () => {
sse.close();
currentSse = null;
currentAbort = null;
showError(d.error || 'Download failed.');
showError(d.error || 'Échec du téléchargement.');
progressWrap.style.display = 'none';
currentFileId = null;
setLoading(downloadBtn, false);
@ -384,7 +303,6 @@ downloadBtn.addEventListener('click', async () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
format_id: formatSelect.value,
file_id: fileId,
}),
signal: abort.signal,
@ -393,11 +311,11 @@ downloadBtn.addEventListener('click', async () => {
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.detail || 'Download failed.');
throw new Error(data.detail || 'Échec du téléchargement.');
}
if (data.status !== 'started') {
throw new Error('Unexpected server response.');
throw new Error('Réponse inattendue du serveur.');
}
} catch (err) {
if (currentSse) {
@ -408,7 +326,7 @@ downloadBtn.addEventListener('click', async () => {
currentAbort = null;
if (err.name !== 'AbortError') {
showError(err.message || 'Download failed.');
showError(err.message || 'Échec du téléchargement.');
progressWrap.style.display = 'none';
}