/** * 00c-download-missing.js — Programmatic download (fetch+blob) via the war.gov UI * * Improved over the previous version: * - Uses fetch() to grab the asset from the same origin (browser cookies + * same-origin policy → Akamai accepts). * - Creates a Blob and triggers `` to control the * filename exactly (no "(1)" duplicates). * - Reads the PDF URL from the modal's download button data and/or thumbnail * src pattern (`/thumbnail/foo.jpg` → `/foo.pdf|.mp4|.jpg`). * - Skips records whose file already exists (best-effort by checking the * expected filename — you can also clear the lists below). * * USAGE (Chrome on https://www.war.gov/UFO/Release-NN/): * 1. Set Chrome download folder to /Users/guto/ufo/raw/ (Settings → Downloads). * For videos, the script will rename to land in /Users/guto/ufo/raw/videos/ * after — just MOVE them manually after this finishes. * 2. Open DevTools → Console. * 3. Paste this whole file. Press Enter. * 4. Chrome prompts "Allow multiple downloads" → click **Allow**. * 5. Wait ~30s (1s between downloads). Files land in Downloads folder. */ (async function downloadMissing() { // ============================================================ // PDFs still missing (Release-01, verified 2026-05-13). // The 28 .VID videos are already in /Users/guto/ufo/raw/videos/ from // a prior bulk download — they share file names like DOD_111688723.mp4. // ============================================================ const TARGETS = [ "record-140", // NASA-UAP-D003 GEMINI 7 TRANSCRIPT 1965 "record-154", // STATE CABLE 003 TBILISI GEORGIA "record-155", // STATE CABLE 004 ASHGABAT TURKMENISTAN "record-156", // STATE CABLE 005 MEXICO ]; console.log(`[dl] ${TARGETS.length} records to download`); // ---------------------------------------------------------------------- const sleep = ms => new Promise(r => setTimeout(r, ms)); function pollUntil(predicate, opts = {}) { const timeout = opts.timeout ?? 4000; const interval = opts.interval ?? 50; return new Promise((resolve, reject) => { const start = Date.now(); const tick = () => { const v = predicate(); if (v) return resolve(v); if (Date.now() - start > timeout) return reject(new Error("timeout")); setTimeout(tick, interval); }; tick(); }); } async function findRowOnAllPages(recordId) { for (let i = 0; i < 25; i++) { const row = document.querySelector(`button.record-row[data-record-id="${recordId}"]`); if (row) return row; const next = document.querySelector("button.pagination-next"); if (!next || next.disabled || next.getAttribute("aria-disabled") === "true") return null; const before = document.querySelector("button.record-row")?.dataset.recordId; next.click(); try { await pollUntil(() => { const f = document.querySelector("button.record-row")?.dataset.recordId; return f && f !== before ? f : null; }); } catch { /* ignore */ } await sleep(150); } return null; } async function goToFirstPage() { const firstBtn = Array.from(document.querySelectorAll(".pagination-button")).find(b => b.innerText.trim() === "1"); if (firstBtn) { firstBtn.click(); await sleep(400); return; } } function buildFilenameFromThumb(thumbUrl, extHint) { // .../thumbnail/foo.jpg → foo + ext const m = thumbUrl.match(/\/thumbnail\/([^?#]+)\.[a-z]+$/i); if (!m) return null; const base = decodeURIComponent(m[1]); return `${base}${extHint}`; } function buildAssetUrlFromThumb(thumbUrl, extHint) { // strip "/thumbnail/" segment, swap extension return thumbUrl.replace("/thumbnail/", "/").replace(/\.[a-z]+$/i, extHint); } async function downloadOne(recordId) { const row = await findRowOnAllPages(recordId); if (!row) { console.warn(` ✗ ${recordId}: row not found`); return false; } row.click(); try { await pollUntil(() => document.querySelector(".record-modal-shell")); } catch { console.warn(` ✗ ${recordId}: modal didn't open`); return false; } const modal = document.querySelector(".record-modal-shell"); const kind = (modal.getAttribute("data-record-kind") || "pdf").toLowerCase(); const title = modal.querySelector("[data-record-modal-title]")?.innerText?.trim() || recordId; const docTypeEl = Array.from(modal.querySelectorAll(".record-modal-fact dd")) .find(d => d.previousElementSibling?.innerText?.trim() === "Document Type"); let ext = ".pdf"; if (docTypeEl) { const raw = docTypeEl.innerText.trim().replace(/[\[\]]/g, "").toLowerCase(); ext = raw.startsWith(".") ? raw : "." + raw; // Normalize uncommon: .vid → .mp4 (guess; site serves mp4 for videos), .img → .jpg if (ext === ".vid") ext = ".mp4"; if (ext === ".img") ext = ".jpg"; } const img = modal.querySelector("#record-main-image, img"); const thumb = img?.src; if (!thumb) { console.warn(` ✗ ${recordId}: no thumbnail src — cannot infer URL`); const close = document.querySelector(".record-modal-close, [data-record-modal-close]"); if (close) close.click(); await sleep(300); return false; } const assetUrl = buildAssetUrlFromThumb(thumb, ext); const filename = buildFilenameFromThumb(thumb, ext) || `${recordId}${ext}`; console.log(` ↓ ${recordId}: fetching "${filename}" from ${assetUrl}`); try { const res = await fetch(assetUrl, { credentials: "include", referrer: location.href }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 5000); console.log(` ✓ ${recordId}: ${filename} (${(blob.size/1024/1024).toFixed(2)} MB)`); } catch (e) { console.warn(` ✗ ${recordId}: fetch failed — ${e.message}`); const close = document.querySelector(".record-modal-close, [data-record-modal-close]"); if (close) close.click(); return false; } const close = document.querySelector(".record-modal-close, [data-record-modal-close]"); if (close) close.click(); await sleep(800); return true; } await goToFirstPage(); let ok = 0, fail = []; for (const id of TARGETS) { const success = await downloadOne(id); if (success) ok++; else fail.push(id); await sleep(500); } console.log(`\n[dl] DONE — ok=${ok}, failed=${fail.length}`); if (fail.length) console.log("failed:", fail); console.log("Move videos from Downloads/ → /Users/guto/ufo/raw/videos/ when done."); })();