const $ = (s) => document.querySelector(s); const filesEl = $("#files"); const jobsEl = $("#jobs"); const countEl = $("#count"); const searchEl = $("#search"); const modal = $("#modal"); let currentFile = null; // fichier sélectionné pour conversion let searchTimer = null; // ---------- Helpers ---------- function human(bytes) { if (!bytes && bytes !== 0) return ""; const u = ["o", "Ko", "Mo", "Go", "To"]; let i = 0, n = bytes; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`; } const langName = { fre: "FR", fra: "FR", fr: "FR", eng: "EN", en: "EN", und: "?" }; // ---------- Recherche / listing ---------- // Redirige vers la connexion si la session a expiré function checkAuth(r) { if (r.status === 401) { window.location = "/login"; return false; } return true; } async function loadFiles(q = "") { const r = await fetch(`/api/browse?q=${encodeURIComponent(q)}`); if (!checkAuth(r)) return; const data = await r.json(); countEl.textContent = data.count ? `${data.count} fichier${data.count > 1 ? "s" : ""}${data.truncated ? "+" : ""}` : "aucun résultat"; filesEl.innerHTML = ""; if (!data.files.length) { filesEl.innerHTML = `
  • Rien trouvé.
  • `; return; } for (const f of data.files) { const li = document.createElement("li"); li.className = "file"; const dir = f.dir && f.dir !== "." ? `${f.dir}/` : ""; li.innerHTML = `
    ${dir}${f.name}
    ${human(f.size)}
    ⬇ Télécharger
    `; li.querySelector(".conv").addEventListener("click", () => openModal(f)); filesEl.appendChild(li); } } searchEl.addEventListener("input", () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => loadFiles(searchEl.value), 250); }); // ---------- Modale d'options ---------- async function openModal(file) { currentFile = file; $("#modal-title").textContent = file.name; const info = $("#modal-info"); const audioSel = $("#opt-audio"); info.textContent = "Analyse du fichier…"; audioSel.innerHTML = ""; modal.classList.remove("hidden"); try { const r = await fetch(`/api/probe?path=${encodeURIComponent(file.path)}`); if (!r.ok) throw new Error((await r.json()).detail || "erreur"); const p = await r.json(); const badges = []; if (p.width) badges.push(`${p.width}×${p.height}`); if (p.hdr) badges.push(`HDR → tone-mapping`); badges.push(`${Math.round(p.duration / 60)} min`); info.innerHTML = badges.join(" · "); p.audio.forEach((a) => { const opt = document.createElement("option"); const lang = langName[a.lang] || a.lang.toUpperCase(); const ch = a.channels ? `${a.channels}ch` : ""; opt.value = a.a_index; opt.textContent = `#${a.a_index} · ${lang} · ${a.codec} ${ch}${a.title ? " — " + a.title : ""}`; if (a.a_index === p.default_audio) opt.selected = true; audioSel.appendChild(opt); }); if (!p.audio.length) { audioSel.innerHTML = ``; } } catch (e) { info.innerHTML = `Impossible d'analyser : ${e.message}`; } } $("#modal-cancel").addEventListener("click", () => modal.classList.add("hidden")); modal.addEventListener("click", (e) => { if (e.target === modal) modal.classList.add("hidden"); }); $("#modal-start").addEventListener("click", async () => { if (!currentFile) return; const body = { path: currentFile.path, resolution: $("#opt-res").value, audio: parseInt($("#opt-audio").value || "0", 10), }; modal.classList.add("hidden"); const r = await fetch("/api/convert", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (r.ok) { await refreshJobs(); } else { alert("Erreur : " + ((await r.json()).detail || r.status)); } }); // ---------- Jobs / progression ---------- function jobRow(j) { const li = document.createElement("li"); li.className = `job ${j.status}`; let right = ""; if (j.status === "done") { right = `⬇ Télécharger l'AVI `; } else if (j.status === "error") { right = ``; } else { right = ``; } const statusTxt = { queued: "En attente…", running: "Conversion…", done: `Terminé · ${human(j.size)}`, error: "Échec", }[j.status] || j.status; li.innerHTML = `
    ${j.source} ${statusTxt}${j.hdr ? " · HDR" : ""}
    ${right}
    `; if (j.status === "error") { const d = document.createElement("div"); d.className = "job-err"; d.textContent = (j.error || "").split("\n").filter(Boolean).slice(-1)[0] || "Erreur inconnue"; li.appendChild(d); } const del = li.querySelector(".del"); if (del) del.addEventListener("click", async () => { await fetch(`/api/jobs/${j.id}`, { method: "DELETE" }); refreshJobs(); }); return li; } async function refreshJobs() { const r = await fetch("/api/jobs"); if (!checkAuth(r)) return; const { jobs } = await r.json(); jobsEl.innerHTML = ""; if (!jobs.length) { jobsEl.innerHTML = `
  • Aucune conversion pour l'instant.
  • `; return; } jobs.forEach((j) => jobsEl.appendChild(jobRow(j))); } // Poll régulier des jobs setInterval(refreshJobs, 1500); // ---------- Utilisateur connecté ---------- async function loadUser() { try { const r = await fetch("/api/me"); const d = await r.json(); if (d.auth && d.user) { $("#user-name").textContent = `👤 ${d.user.name}`; $("#user").classList.remove("hidden"); } } catch { /* ignore */ } } // Init loadUser(); loadFiles(); refreshJobs();