Captura integral do acervo da Brasiliana Fotográfica (Biblioteca Nacional) para s3://brasiliana: 13.060 itens, 12.985 com imagem de preservação, 13.059 com miniatura derivada, 2.137 cópias legadas e 636 posts do blog. 41.444 objetos no ledger. Os 75 itens sem preservação estão em errors.md: a página do item responde 200, mas a origem serve o bitstream com HTTP 500. Nenhum fica fora do mural, porque todos têm thumbnail. Não versionado, por viver no bucket ou ser reconstruível: catálogo, posts .jsonl, atlas .webp, sqlite de estado e logs. Credenciais ficam de fora pelo .gitignore.
203 lines
8.7 KiB
JavaScript
203 lines
8.7 KiB
JavaScript
/* ===================================================================
|
|
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()');
|
|
})();
|