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:
@@ -0,0 +1,142 @@
|
||||
/* ===================================================================
|
||||
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()'); })();
|
||||
})();
|
||||
Reference in New Issue
Block a user