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.
143 lines
6.0 KiB
JavaScript
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()'); })();
|
|
})();
|