feat: acervo Brasiliana Fotográfica coletado e mural 3D no ar

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.
This commit is contained in:
rodado
2026-09-13 20:17:02 +02:00
commit 290c4cc21d
690 changed files with 81187 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
/* ===================================================================
Reparo do coletor.js -- reprocessa itens que ficaram marcados com
erro (ex.: queda de rede no meio da colheita).
POR QUE PRECISA: coletar() grava {handle, erro} em 'itens' quando
uma requisicao falha. Isso conta como "feito" pro loop principal --
ele nao tenta de novo sozinho. Falhas de rede (TypeError: Failed to
fetch) tambem nao disparam a pausa de 60s (so HTTP 4xx/5xx disparam),
entao uma queda pode gerar uma rajada de erros em vez de so alguns.
COMO USAR
1. Rode DEPOIS que await BRAS.status() mostrar fase 'concluido'
(pra nao competir por rede com o loop principal).
2. Cole este arquivo inteiro no mesmo console (mesma aba/origem).
3. Acompanhe pelo proprio console.log -- ele avisa quantos achou
e reporta ok/erro no final.
4. Se ainda sobrar erro (rede caiu nesse meio-tempo tambem), rode
de novo -- e idempotente.
=================================================================== */
(async () => {
const BASE = 'https://brasilianafotografica.bn.gov.br/brasiliana';
const PREFIX= '20.500.12156.1';
const PAUSA = 2000;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const jit = () => PAUSA + Math.floor(Math.random() * 600);
const db = await new Promise((res, rej) => {
const q = indexedDB.open('bras', 1);
q.onsuccess = () => res(q.result); q.onerror = () => rej(q.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 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 getAll = st => new Promise((res, rej) => {
const r = db.transaction(st).objectStore(st).getAll();
r.onsuccess = () => res(r.result); r.onerror = () => rej(r.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();
}
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 todos = await getAll('itens');
const falhados = todos.filter(x => x.erro);
console.log(`${falhados.length} itens com erro -- reprocessando`);
let ok = 0, erro = 0;
for (const item of falhados) {
const h = item.handle.split('/').pop();
const com = await get('fila', h); // recupera o nome da comunidade
try {
const html = await pegar(`${BASE}/handle/${PREFIX}/${h}?show=full`);
await put('itens', h, extrair(html, h, com));
ok++;
} catch (e) {
await put('itens', h, {handle: item.handle, erro: String(e).slice(0, 80)});
erro++;
if (/HTTP 4|HTTP 5/.test(String(e))) await sleep(60000);
}
await sleep(jit());
}
console.log(`\nReparo concluido. ok=${ok} ainda-com-erro=${erro}`);
})();