Support hour-long uploads and videos: raise body limit, stream uploads to disk, cache up to 3 h, add deferred-ideas note

This commit is contained in:
Claude
2026-10-01 18:48:37 +00:00
parent 9d74769029
commit 08e95814e5
8 changed files with 232 additions and 66 deletions

View File

@@ -636,25 +636,37 @@
$('upForm').addEventListener('submit', (e) => {
e.preventDefault();
const f = $('upFile').files[0]; if (!f) return;
const fd = new FormData();
fd.append('file', f);
if ($('upArt').files[0]) fd.append('art', $('upArt').files[0]);
if ($('upTitle').value.trim()) fd.append('title', $('upTitle').value.trim());
if ($('upArtist').value.trim()) fd.append('artist', $('upArtist').value.trim());
const qs = new URLSearchParams({ name: f.name });
if ($('upTitle').value.trim()) qs.set('title', $('upTitle').value.trim());
if ($('upArtist').value.trim()) qs.set('artist', $('upArtist').value.trim());
const cover = $('upArt').files[0];
const msg = $('upProgress'); msg.className = 'msg'; msg.textContent = `Uploading ${f.name}…`;
// The raw file is the request body (streamed from disk by the browser, to
// disk by the server) so hour-long, multi-GB videos need no memory.
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/admin/uploads'); xhr.withCredentials = true;
xhr.upload.onprogress = (ev) => { if (ev.lengthComputable) msg.textContent = `Uploading ${f.name}… ${Math.round((ev.loaded / ev.total) * 100)}%`; };
xhr.onload = () => {
xhr.open('PUT', '/api/admin/uploads/stream?' + qs); xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
const t0 = Date.now();
xhr.upload.onprogress = (ev) => {
if (!ev.lengthComputable) return;
const pct = Math.round((ev.loaded / ev.total) * 100), mbps = ev.loaded / 1048576 / Math.max(1, (Date.now() - t0) / 1000);
msg.textContent = `Uploading ${f.name}… ${pct}% · ${(ev.loaded / 1048576).toFixed(0)} of ${(ev.total / 1048576).toFixed(0)} MB · ${mbps.toFixed(1)} MB/s${pct >= 100 ? ' — processing…' : ''}`;
};
xhr.onload = async () => {
let j = {}; try { j = JSON.parse(xhr.responseText); } catch { /* non-JSON */ }
if (xhr.status === 200 && j.ok) {
let artNote = j.upload.art ? ', cover art' : '';
if (cover) {
const ar = await fetch(`/api/admin/uploads/${j.upload.id}/art`, { method: 'PUT', credentials: 'same-origin', body: cover }).then((r) => r.json()).catch(() => ({}));
artNote = ar.ok ? ', cover art' : ', cover image unusable';
}
msg.className = 'msg ok';
msg.textContent = `Added “${j.upload.title}” (${j.upload.kind}${j.upload.art ? ', cover art' : ''}${j.lyricLines ? `, ${j.lyricLines} lyric lines` : ''}).`;
$('upForm').reset(); loadUploads(); loadRecent();
} else { msg.className = 'msg err'; msg.textContent = j.error || `Upload failed (HTTP ${xhr.status})`; }
msg.textContent = `Added “${j.upload.title}” (${j.upload.kind}${artNote}${j.lyricLines ? `, ${j.lyricLines} lyric lines` : ''}).`;
$('upForm').reset(); loadUploads(); loadRecent(); loadSongs();
} else { msg.className = 'msg err'; msg.textContent = j.error || (xhr.status === 413 ? 'File is too large (limit 4 GB)' : `Upload failed (HTTP ${xhr.status})`); }
};
xhr.onerror = () => { msg.className = 'msg err'; msg.textContent = 'Upload failed — network error'; };
xhr.send(fd);
xhr.send(f);
});
$('upTable').addEventListener('click', async (e) => {
const b = e.target.closest('button'); if (!b) return;