()
| 564 | |
| 565 | // Submit an extraction (URL or file) and follow it to completion. |
| 566 | async function startExtraction() { |
| 567 | const stems = Object.keys(state.selected).filter((k) => state.selected[k]); |
| 568 | if (!stems.length) { toast("Pick at least one stem."); return; } |
| 569 | const file = state.extractFile; |
| 570 | const url = (state.extractUrl || "").trim(); |
| 571 | if (!file && !url) { toast("Paste a URL or choose a file first."); return; } |
| 572 | |
| 573 | let init, title; |
| 574 | if (file) { |
| 575 | const fd = new FormData(); |
| 576 | fd.append("file", file); |
| 577 | fd.append("stems", JSON.stringify(stems)); |
| 578 | init = { method: "POST", body: fd }; |
| 579 | title = file.name; |
| 580 | } else { |
| 581 | init = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, stems }) }; |
| 582 | title = url; |
| 583 | } |
| 584 | |
| 585 | state.extractJob = { id: null, title, status: "queued", progress: 0, stage: "Queued" }; |
| 586 | render(); |
| 587 | |
| 588 | let jobId; |
| 589 | try { |
| 590 | const res = await fetch("/api/jobs", init); |
| 591 | const data = await res.json().catch(() => ({})); |
| 592 | if (!res.ok) throw new Error(data.detail || res.statusText); |
| 593 | jobId = data.job_id; |
| 594 | } catch (e) { |
| 595 | console.warn("[mobile] extraction submit failed:", e); |
| 596 | state.extractJob = null; |
| 597 | toast(`Couldn't start: ${e.message}`); |
| 598 | render(); |
| 599 | return; |
| 600 | } |
| 601 | |
| 602 | state.extractJob.id = jobId; |
| 603 | state.extractFile = null; |
| 604 | state.extractUrl = ""; |
| 605 | followExtraction(jobId); |
| 606 | render(); |
| 607 | } |
| 608 | |
| 609 | function _onExtractState(jobId, s) { |
| 610 | if (!state.extractJob || state.extractJob.id !== jobId) return false; |
no test coverage detected