/* =================================================================== Coletor da Brasiliana Fotografica -- RETOMADA (resume + reparo juntos). Diferenca do coletor.js original: coletar() considera "feito" so quem tem registro SEM campo .erro. Handles nunca tentados E handles que falharam antes (rede caiu, HTTP 4xx/5xx) entram juntos na mesma passada -- nao precisa colar coletor.js e depois coletor-reparo.js separado. COMO USAR 1. Abra https://brasilianafotografica.bn.gov.br/brasiliana/ (mesma origem de sempre -- reaproveita o IndexedDB existente) 2. Cole este arquivo inteiro no console 3. Ele pula a descoberta (fila ja esta cheia) e colhe so o que falta: pendentes + os que tinham erro 4. await BRAS.status() pra acompanhar; await BRAS.exportar() no fim =================================================================== */ (() => { const BASE = 'https://brasilianafotografica.bn.gov.br/brasiliana'; const PREFIX= '20.500.12156.1'; const RPP = 100; const PAUSA = 2000; // ms entre requisicoes. Nao baixe disso. const COM = { '2':'Biblioteca Nacional', '1753':'Instituto Moreira Salles', '3540':'Arquivo Geral da Cidade do Rio de Janeiro', '3541':'Diretoria do Patrimonio Historico e Documentacao da Marinha', '4342':'Leibniz-Institut fuer Laenderkunde, Leipzig', '4884':'Arquivo Nacional', '4886':'Museu da Republica', '5018':'Fiocruz', '6142':'Museu Historico Nacional', '6660':'Fundacao Joaquim Nabuco', '7072':'Museu Aeroespacial', '11464':'Escola de Ciencias Sociais (FGV CPDOC)', '12796':'Instituto Historico e Geografico Brasileiro', '13099':'Instituto Brasileiro de Geografia e Estatistica', '14087':'Museu Paraense Emilio Goeldi', }; const ESPERADO = 13036; const sleep = ms => new Promise(r => setTimeout(r, ms)); const jit = () => PAUSA + Math.floor(Math.random() * 600); // ---------- IndexedDB ---------- let db; const abrir = () => new Promise((res, rej) => { const q = indexedDB.open('bras', 1); q.onupgradeneeded = e => { const d = e.target.result; if (!d.objectStoreNames.contains('fila')) d.createObjectStore('fila'); if (!d.objectStoreNames.contains('itens')) d.createObjectStore('itens'); }; q.onsuccess = e => res(e.target.result); q.onerror = () => rej(q.error); }); const put = (st, k, v) => new Promise((res, rej) => { const t = db.transaction(st, 'readwrite'); t.objectStore(st).put(v, k); t.oncomplete = res; t.onerror = () => rej(t.error); }); const putMuitos = (st, pares) => new Promise((res, rej) => { const t = db.transaction(st, 'readwrite'), o = t.objectStore(st); for (const [k, v] of pares) o.put(v, k); t.oncomplete = res; t.onerror = () => rej(t.error); }); const keys = st => new Promise((res, rej) => { const r = db.transaction(st).objectStore(st).getAllKeys(); r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); const get = (st, k) => new Promise(res => { const r = db.transaction(st).objectStore(st).get(k); r.onsuccess = () => res(r.result); r.onerror = () => res(undefined); }); const getAll = st => new Promise((res, rej) => { const r = db.transaction(st).objectStore(st).getAll(); r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); // so as chaves cujo valor NAO tem .erro -- feito de verdade const chavesOk = () => new Promise((res, rej) => { const s = new Set(); const req = db.transaction('itens').objectStore('itens').openCursor(); req.onsuccess = e => { const c = e.target.result; if (c) { if (!c.value.erro) s.add(c.key); c.continue(); } else res(s); }; req.onerror = () => rej(req.error); }); async function pegar(u) { const r = await fetch(u, {credentials: 'same-origin'}); if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); } // ---------- extracao ---------- function extrair(html, h, com) { const d = new DOMParser().parseFromString(html, 'text/html'); const dc = [...d.querySelectorAll('meta[name]')] .filter(m => /^(DC|DCTERMS|citation)/.test(m.name)) .map(m => ({name: m.name, value: m.content})); const a = n => (dc.find(x => x.name === n) || {}).value || null; const tb = [...d.querySelectorAll('table.detailtable tr, table.ds-includeSet-table tr')] .map(tr => { const td = [...tr.querySelectorAll('td')].map(c => c.textContent.trim()); return td.length >= 2 ? {field: td[0], value: td[1]} : null; }) .filter(Boolean); const leg = dc.filter(x => x.name === 'DC.identifier' && /objdigital/.test(x.value || '')) .map(x => x.value)[0] || null; return { handle: `${PREFIX}/${h}`, source_url: `${BASE}/handle/${PREFIX}/${h}?show=full`, fetched_at: new Date().toISOString(), title: a('DC.title'), community: com, preservation_url: a('citation_pdf_url'), legacy_url: leg, rights: (tb.find(t => /^dc\.rights/i.test(t.field)) || {}).value || null, dublin_core: dc, full_table: tb, }; } const S = {fase: 'iniciando', atual: '', colhidos: 0, erros: 0, parar: false, t0: Date.now()}; // ---------- fase 1: enumerar (so roda se a fila estiver incompleta) ---------- async function descobrir() { S.fase = 'enumerando'; const RE = new RegExp('handle/' + PREFIX.replace(/\./g, '\\.') + '/(\\d+)', 'g'); for (const [h, nome] of Object.entries(COM)) { if (S.parar) return; S.atual = nome; let off = 0, total = null, vistos = new Set(); while (!S.parar) { let html; try { html = await pegar(`${BASE}/handle/${PREFIX}/${h}/browse?type=title&rpp=${RPP}&offset=${off}`); } catch (e) { console.warn(` ${nome} offset=${off}: ${e.message} -- pausa de 30s`); await sleep(30000); continue; } if (total === null) { const m = html.replace(/<[^>]+>/g, ' ') .match(/no momento\D{0,24}[\d.,]+\s*-\s*[\d.,]+\s*de\s*([\d.,]+)/i); total = m ? +m[1].replace(/[.,]/g, '') : null; } const ach = new Set([...html.matchAll(RE)].map(m => m[1])); ach.delete(h); let novos = 0; for (const x of ach) if (!vistos.has(x)) { vistos.add(x); novos++; } await putMuitos('fila', [...ach].map(x => [x, nome])); off += RPP; if (!novos || (total && off >= total)) break; await sleep(jit()); } console.log(` ${nome.padEnd(46)} ${String(vistos.size).padStart(6)}`); await sleep(jit()); } const n = (await keys('fila')).length; console.log(`\nEnumerados: ${n} (esperado ${ESPERADO})`); } // ---------- fase 2: metadados (pendentes + com erro, juntos) ---------- async function coletar() { S.fase = 'colhendo'; const fila = await keys('fila'), ok = await chavesOk(); const pend = fila.filter(h => !ok.has(h)); console.log(`${pend.length} itens a colher/reparar (~${(pend.length * PAUSA / 3600000).toFixed(1)} h)`); for (let i = 0; i < pend.length; i++) { if (S.parar) { S.fase = 'parado'; return; } const h = pend[i]; S.atual = `${i + 1}/${pend.length}`; try { const html = await pegar(`${BASE}/handle/${PREFIX}/${h}?show=full`); await put('itens', h, extrair(html, h, await get('fila', h))); S.colhidos++; } catch (e) { await put('itens', h, {handle: `${PREFIX}/${h}`, erro: String(e).slice(0, 80)}); S.erros++; if (/HTTP 4|HTTP 5/.test(String(e))) { console.warn(` ${h}: ${e.message} -- pausa de 60s`); await sleep(60000); } } if (i % 100 === 0) console.log(` ${i}/${pend.length} ok=${S.colhidos} erro=${S.erros}`); await sleep(jit()); } S.fase = 'concluido'; console.log(`\nConcluido. ok=${S.colhidos} erro=${S.erros}. Agora: await BRAS.exportar()`); } // ---------- exportar ---------- async function exportar() { const todos = await getAll('itens'); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([todos.map(o => JSON.stringify(o)).join('\n')], {type: 'application/x-ndjson'})); a.download = 'brasiliana-catalogo.jsonl'; document.body.appendChild(a); a.click(); a.remove(); console.log(`exportadas ${todos.length} linhas para a pasta Downloads`); return todos.length; } window.BRAS = { status: async () => { const naFila = (await keys('fila')).length, ok = await chavesOk(); const h = (Date.now() - S.t0) / 3600000; return {fase: S.fase, atual: S.atual, naFila, noBanco: ok.size, erros: S.erros, restam: naFila - ok.size, horas: +h.toFixed(2)}; }, parar: () => { S.parar = true; console.log('parando apos a requisicao atual'); }, retomar: () => { S.parar = false; coletar(); }, exportar, limpar: () => indexedDB.deleteDatabase('bras'), }; (async () => { db = await abrir(); if ((await keys('fila')).length < ESPERADO) await descobrir(); await coletar(); })(); console.log('BRAS retomado (pendentes + erros juntos). Acompanhe com: await BRAS.status()'); })();