Commit ·
fb5cad2
1
Parent(s): 9d60340
Say when a take has been cleared instead of failing the download
Browse filesTakes live in memory, so a Space redeploy or restart drops them and the
download links 404 — surfacing as the browser's opaque 'File wasn't
available on site'. Download clicks now probe the stored take with a
one-byte Range request first and, if it is gone, explain what happened
and why in the app rather than handing the user a dead link. Playback
load failures report the same thing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- static/app.js +42 -2
static/app.js
CHANGED
|
@@ -1533,14 +1533,54 @@ function noteExportStarted(kind) {
|
|
| 1533 |
noteExportStarted.timer = setTimeout(() => { el.polishStatus.hidden = true; }, 20000);
|
| 1534 |
}
|
| 1535 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1536 |
[["downloadBtn", "WAV"], ["stageDownloadBtn", "WAV"],
|
| 1537 |
["downloadMp3Btn", "MP3"], ["stageDownloadMp3Btn", "MP3"]].forEach(([id, kind]) => {
|
| 1538 |
-
el[id].addEventListener("click", () => {
|
| 1539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1540 |
if (kind === "MP3" || !polishIsDefault()) noteExportStarted(kind);
|
|
|
|
|
|
|
| 1541 |
});
|
| 1542 |
});
|
| 1543 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1544 |
function polishTouched() {
|
| 1545 |
ensureAudioGraph();
|
| 1546 |
if (polish.ctx && polish.ctx.state === "suspended") polish.ctx.resume();
|
|
|
|
| 1533 |
noteExportStarted.timer = setTimeout(() => { el.polishStatus.hidden = true; }, 20000);
|
| 1534 |
}
|
| 1535 |
|
| 1536 |
+
const TAKE_GONE_MESSAGE =
|
| 1537 |
+
"This take is no longer on the server — it is held in memory, so a Space restart or " +
|
| 1538 |
+
"redeploy clears it. Generate a new one; download it before deploying next time.";
|
| 1539 |
+
|
| 1540 |
+
function showTakeGone() {
|
| 1541 |
+
el.polishStatus.hidden = false;
|
| 1542 |
+
el.polishStatus.textContent = TAKE_GONE_MESSAGE;
|
| 1543 |
+
el.dockEmpty.hidden = false;
|
| 1544 |
+
el.dockEmpty.textContent = TAKE_GONE_MESSAGE;
|
| 1545 |
+
setStatus("error", "The finished audio is no longer available on the server.");
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
// A dead link would otherwise surface as the browser's opaque "File wasn't
|
| 1549 |
+
// available on site". Probe the stored take (one byte) before letting the
|
| 1550 |
+
// download proceed, and say plainly when it has been cleared.
|
| 1551 |
[["downloadBtn", "WAV"], ["stageDownloadBtn", "WAV"],
|
| 1552 |
["downloadMp3Btn", "MP3"], ["stageDownloadMp3Btn", "MP3"]].forEach(([id, kind]) => {
|
| 1553 |
+
el[id].addEventListener("click", async (event) => {
|
| 1554 |
+
const anchor = el[id];
|
| 1555 |
+
if (anchor.dataset.verified === "1") {
|
| 1556 |
+
anchor.dataset.verified = "";
|
| 1557 |
+
return; // second pass: this is the real download
|
| 1558 |
+
}
|
| 1559 |
+
if (!polish.audioId) return;
|
| 1560 |
+
event.preventDefault();
|
| 1561 |
+
let alive = false;
|
| 1562 |
+
try {
|
| 1563 |
+
// Probe the raw take, never the export URL — that would start a render.
|
| 1564 |
+
const probe = await fetch(`/api/audio/${polish.audioId}`, { headers: { Range: "bytes=0-0" } });
|
| 1565 |
+
alive = probe.ok || probe.status === 206;
|
| 1566 |
+
} catch {
|
| 1567 |
+
alive = false;
|
| 1568 |
+
}
|
| 1569 |
+
if (!alive) {
|
| 1570 |
+
showTakeGone();
|
| 1571 |
+
return;
|
| 1572 |
+
}
|
| 1573 |
if (kind === "MP3" || !polishIsDefault()) noteExportStarted(kind);
|
| 1574 |
+
anchor.dataset.verified = "1";
|
| 1575 |
+
anchor.click();
|
| 1576 |
});
|
| 1577 |
});
|
| 1578 |
|
| 1579 |
+
// Same story if playback itself can't load the take.
|
| 1580 |
+
el.resultAudio.addEventListener("error", () => {
|
| 1581 |
+
if (el.resultAudio.getAttribute("src")) showTakeGone();
|
| 1582 |
+
});
|
| 1583 |
+
|
| 1584 |
function polishTouched() {
|
| 1585 |
ensureAudioGraph();
|
| 1586 |
if (polish.ctx && polish.ctx.state === "suspended") polish.ctx.resume();
|