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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user