()
| 2109 | // Only done↔unavailable are reconciled, so in-progress imports are never |
| 2110 | // mis-flagged (they aren't on the server's done-list yet). |
| 2111 | async function resyncLibrary() { |
| 2112 | const statusEl = libraryEditor?.querySelector(".library-editor-status"); |
| 2113 | const syncBtn = libraryEditor?.querySelector(".library-editor-sync"); |
| 2114 | if (statusEl) statusEl.textContent = "Syncing…"; |
| 2115 | if (syncBtn) syncBtn.disabled = true; |
| 2116 | |
| 2117 | try { |
| 2118 | const res = await fetch("/api/jobs", { cache: "no-store" }); |
| 2119 | if (!res.ok) throw new Error(`status ${res.status}`); |
| 2120 | const jobs = await res.json(); |
| 2121 | const serverIds = new Set(jobs.map((j) => j.job_id)); |
| 2122 | |
| 2123 | await syncWithServer(); // forward: pull in any new server jobs |
| 2124 | |
| 2125 | const trashIds = new Set(getTrashFolder()?.items || []); |
| 2126 | for (const [id, t] of Object.entries(tracks)) { |
| 2127 | if (trashIds.has(id)) continue; |
| 2128 | if (t.status === "done" && !serverIds.has(id)) t.status = "unavailable"; |
| 2129 | else if (t.status === "unavailable" && serverIds.has(id)) t.status = "done"; |
| 2130 | } |
| 2131 | saveState(); |
| 2132 | render(); |
| 2133 | if (libraryEditor) renderLibraryRows(libraryEditor.querySelector(".library-editor-body")); |
| 2134 | |
| 2135 | // Collect what's still unavailable; auto-restore the ones with a URL source. |
| 2136 | const unavailable = Object.entries(tracks) |
| 2137 | .filter(([id, t]) => !trashIds.has(id) && t.status === "unavailable") |
| 2138 | .map(([, t]) => t); |
| 2139 | const restorable = unavailable.filter((t) => t.sourceUrl && !t.sourceUrl.startsWith("local:")); |
| 2140 | |
| 2141 | if (restorable.length) { |
| 2142 | // Re-import each from its source. Close the editor so the studio overlay |
| 2143 | // shows progress; restore sequentially (single active studio job). |
| 2144 | closeLibraryEditor(); |
| 2145 | for (const t of restorable) { |
| 2146 | const jobId = await importFromUrl(t.sourceUrl, { |
| 2147 | title: t.title, |
| 2148 | stems: t.selectedStems, |
| 2149 | }); |
| 2150 | if (jobId) await waitForJobTerminal(jobId); |
| 2151 | } |
| 2152 | return; |
| 2153 | } |
| 2154 | |
| 2155 | // Nothing auto-restorable left — show the out-of-sync count (local-file |
| 2156 | // tracks can't be re-fetched and stay flagged). |
| 2157 | refreshLibrarySyncSummary(); |
| 2158 | } catch (e) { |
| 2159 | console.warn("[catalog] resync failed:", e); |
| 2160 | if (statusEl) statusEl.textContent = "Sync failed — check your connection."; |
| 2161 | } finally { |
| 2162 | if (syncBtn) syncBtn.disabled = false; |
| 2163 | } |
| 2164 | } |
| 2165 | |
| 2166 | function wireSettingsMenu() { |
| 2167 | const btn = document.getElementById("settingsBtn"); |
no test coverage detected