Fork to yt-mp3-ui
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
84a8e0faa7
commit
12703fa0e0
10 changed files with 198 additions and 302 deletions
|
|
@ -9,8 +9,6 @@ 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');
|
||||
|
|
@ -50,7 +48,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 +77,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 +103,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 +110,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 +134,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 +154,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 +169,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 +195,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 +208,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 +227,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 +241,7 @@ downloadBtn.addEventListener('click', async () => {
|
|||
} else if (d.status === 'finished') {
|
||||
setProgress(
|
||||
100,
|
||||
'<span class="pct">100%</span> <span class="meta">Processing…</span>'
|
||||
'<span class="pct">100%</span> <span class="meta">Traitement…</span>'
|
||||
);
|
||||
} else if (d.status === 'ready') {
|
||||
sse.close();
|
||||
|
|
@ -340,7 +251,7 @@ downloadBtn.addEventListener('click', async () => {
|
|||
if (currentFileId === fileId) {
|
||||
setProgress(
|
||||
100,
|
||||
'<span class="pct">✓</span> <span class="meta">Done — saving to your downloads</span>'
|
||||
'<span class="pct">✓</span> <span class="meta">Terminé — enregistrement dans vos téléchargements</span>'
|
||||
);
|
||||
|
||||
window.location.href = `${API}/file/${fileId}`;
|
||||
|
|
@ -363,7 +274,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 +295,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 +303,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 +318,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';
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue