Files
brasiliano/pipeline/coletor-wordpress.js
T
rodado 290c4cc21d 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.
2026-09-13 20:17:02 +02:00

143 lines
6.0 KiB
JavaScript

/* ===================================================================
Coletor dos posts editoriais do WordPress (raiz do dominio, separado
do /brasiliana/ DSpace). Roda igual o coletor.js -- dentro da pagina,
fetch() same-origin, grava em IndexedDB proprio ('bras_wp').
Nao baixa imagem nenhuma: as fotos dos posts ja apontam (em miniatura)
pros mesmos handles que o acervo principal ja cobre. Só o texto e o
mapeamento pra handles tem valor novo aqui.
COMO USAR
1. Cole este arquivo na mesma aba (ou qualquer aba da mesma origem)
2. await WP.descobrir() -- enumera todos os posts (categorias x paginacao)
3. await WP.colher() -- busca o conteudo de cada post
4. await WP.status()
5. await WP.exportar() -- baixa wordpress-posts.jsonl
=================================================================== */
(() => {
const BASE = 'https://brasilianafotografica.bn.gov.br';
const CATS = ["25","26","311","5","9","35","189","1"];
const PAUSA = 1500;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const jit = () => PAUSA + Math.floor(Math.random() * 500);
let db;
const abrir = () => new Promise((res, rej) => {
const q = indexedDB.open('bras_wp', 1);
q.onupgradeneeded = e => { const d = e.target.result;
if (!d.objectStoreNames.contains('fila')) d.createObjectStore('fila');
if (!d.objectStoreNames.contains('posts')) d.createObjectStore('posts'); };
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 getAll = st => new Promise((res, rej) => {
const r = db.transaction(st).objectStore(st).getAll();
r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error);
});
const S = {fase: 'ocioso', atual: '', colhidos: 0, erros: 0, parar: false};
async function descobrir() {
S.fase = 'descobrindo';
for (const cat of CATS) {
if (S.parar) return;
S.atual = `categoria ${cat}`;
let pagina = 1, vazias = 0;
while (vazias < 1 && pagina <= 100) {
if (S.parar) return;
const url = pagina === 1 ? `${BASE}/?cat=${cat}` : `${BASE}/?cat=${cat}&paged=${pagina}`;
let html;
try { const r = await fetch(url, {credentials:'same-origin'}); html = await r.text(); }
catch(e) { vazias++; pagina++; await sleep(2000); continue; }
const d = new DOMParser().parseFromString(html, 'text/html');
const ids = [...d.querySelectorAll('article')]
.map(a => a.id?.match(/post-(\d+)/)?.[1]).filter(Boolean);
if (!ids.length) { vazias++; pagina++; await sleep(300); continue; }
await putMuitos('fila', ids.map(id => [id, cat]));
pagina++;
await sleep(jit());
}
}
const n = (await keys('fila')).length;
console.log(`Descoberta: ${n} posts unicos`);
}
function limpaTexto(el) {
const clone = el.cloneNode(true);
clone.querySelectorAll('.sharify-container, script, style').forEach(e => e.remove());
return clone.textContent.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
}
async function colherUm(id) {
const r = await fetch(`${BASE}/?p=${id}`, {credentials: 'same-origin'});
if (!r.ok) throw new Error('HTTP ' + r.status);
const html = await r.text();
const d = new DOMParser().parseFromString(html, 'text/html');
const titulo = d.querySelector('.entry-title')?.textContent?.trim() || d.title;
const autor = d.querySelector('.author')?.textContent?.trim() || null;
const dataIso = d.querySelector('time')?.getAttribute('datetime') || null;
const categorias = [...new Set([...d.querySelectorAll('a[href*="?cat="]')].map(a => a.textContent.trim()).filter(Boolean))];
const contentEl = d.querySelector('.entry-content');
const corpo = contentEl ? limpaTexto(contentEl) : null;
const handles = [...new Set([...html.matchAll(/handle\/(20\.500\.12156\.1)\/(\d+)/g)].map(m => `${m[1]}/${m[2]}`))];
return {
id, url: `${BASE}/?p=${id}`, titulo, autor, data_iso: dataIso,
categorias, corpo, handles_referenciados: handles, fetched_at: new Date().toISOString(),
};
}
async function colher() {
S.fase = 'colhendo';
const fila = await keys('fila'), feitos = new Set(await keys('posts'));
const pend = fila.filter(id => !feitos.has(id));
console.log(`${pend.length} posts a colher`);
for (let i = 0; i < pend.length; i++) {
if (S.parar) { S.fase = 'parado'; return; }
const id = pend[i];
S.atual = `${i + 1}/${pend.length}`;
try {
const post = await colherUm(id);
await put('posts', id, post);
S.colhidos++;
} catch (e) {
await put('posts', id, {id, erro: String(e).slice(0, 80)});
S.erros++;
}
if (i % 25 === 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}`);
}
async function exportar() {
const todos = await getAll('posts');
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 = 'wordpress-posts.jsonl';
document.body.appendChild(a); a.click(); a.remove();
console.log(`exportados ${todos.length} posts`);
return todos.length;
}
window.WP = {
descobrir, colher, exportar,
status: async () => ({...S, naFila: (await keys('fila')).length, noBanco: (await keys('posts')).length}),
parar: () => { S.parar = true; },
};
(async () => { db = await abrir(); console.log('WP pronto. await WP.descobrir() depois await WP.colher()'); })();
})();