/* =================================================================== v2 -- a v1 confiava cegamente que seq_master+1 e a miniatura, so descartando se viesse GRANDE demais. Bug real (2026-09-10): pra muitos itens (varias instituicoes -- visto em Marinha e IMS), essa sequencia cai no bitstream de LICENCA do DSpace (texto puro, poucas centenas de bytes) em vez da miniatura. 62% dos thumbs de um lote sairam errados assim -- pequenos o bastante pra passar no filtro de tamanho, mas nao sao imagem nenhuma. FIX: valida os magic bytes (JPEG/PNG) antes de aceitar. Se a primeira tentativa (seq+1) nao for imagem, tenta seq+2 e seq+3 (caso a licenca esteja no meio). Se nenhuma bater, cai pro metodo confiavel do achado 2.7 (extrai o da pagina do item). O subidor.py TAMBEM valida antes de subir agora (defesa em camadas) -- mas o ideal e nao gerar o lixo em primeiro lugar. =================================================================== */ (() => { const BASE = 'https://brasilianafotografica.bn.gov.br/brasiliana'; const PREFIX = '20.500.12156.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', 1); q.onsuccess = () => res(q.result); q.onerror = () => rej(q.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', feito: 0, erros: 0, total: 0, atual: '', parar: false}; const FEITOS = new Set(JSON.parse(localStorage.getItem('bras_thumbs_v2') || '[]')); const marcar = h => { FEITOS.add(h); localStorage.setItem('bras_thumbs_v2', JSON.stringify([...FEITOS])); }; function seqDoUrl(u) { const m = u.match(/\/bitstream\/[^/]+\/[^/]+\/(\d+)\//); return m ? +m[1] : null; } async function ehImagem(blob) { const buf = new Uint8Array(await blob.slice(0, 8).arrayBuffer()); if (buf[0] === 0xFF && buf[1] === 0xD8) return true; // JPEG if (buf[0]===0x89 && buf[1]===0x50 && buf[2]===0x4E && buf[3]===0x47) return true; // PNG return false; } async function tentaSequencia(handle, seq) { const url = `${BASE}/bitstream/${PREFIX}/${handle}/${seq}/thumb.jpg`; const r = await fetch(url, {credentials: 'same-origin'}); if (!r.ok) return null; const b = await r.blob(); if (b.size > 2 * 1024 * 1024) return null; // grande demais pra thumb if (!(await ehImagem(b))) return null; // achado 2026-09-10: pode ser bitstream de licenca return {blob: b, nomeSeq: seq}; } async function viaImgDaPagina(handle) { const r = await fetch(`${BASE}/handle/${PREFIX}/${handle}`, {credentials: 'same-origin'}); if (!r.ok) return null; const html = await r.text(); const d = new DOMParser().parseFromString(html, 'text/html'); const img = d.querySelector('.file-wrapper img'); if (!img) return null; let u; try { u = new URL(img.getAttribute('src'), location.href); } catch(e) { return null; } const rr = await fetch(u.href, {credentials: 'same-origin'}); if (!rr.ok) return null; const b = await rr.blob(); if (!(await ehImagem(b))) return null; return {blob: b, nomeSeq: 'pagina'}; } async function alvos() { const itens = await getAll('itens'); return itens.filter(o => { if (o.erro || !o.preservation_url) return false; const seqs = (o.all_preservation_urls || [o.preservation_url]).map(seqDoUrl).filter(Boolean); return seqs.length > 0; }); } async function iniciar() { S.parar = false; S.fase = 'baixando'; const lista = (await alvos()).filter(o => !FEITOS.has(o.handle)); S.total = lista.length; console.log(`${lista.length} miniaturas a checar/baixar (v2, com validacao)`); for (let i = 0; i < lista.length; i++) { if (S.parar) { S.fase = 'parado'; return; } const o = lista[i]; const h = o.handle.split('/').pop(); S.atual = `${i + 1}/${lista.length}`; try { const seqs = (o.all_preservation_urls || [o.preservation_url]).map(seqDoUrl).filter(Boolean); const maxSeq = Math.max(...seqs); let achado = null; for (const seq of [maxSeq + 1, maxSeq + 2, maxSeq + 3]) { achado = await tentaSequencia(h, seq); if (achado) break; await sleep(300); } if (!achado) achado = await viaImgDaPagina(h); if (!achado) throw new Error('nenhuma miniatura valida encontrada'); const nome = `THUMB__thumb_${achado.nomeSeq}.jpg`; const arqNome = `BRAS__${o.handle.replace(/\//g, '_')}__${nome}`; const a = document.createElement('a'); a.href = URL.createObjectURL(achado.blob); a.download = arqNome; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 30000); marcar(o.handle); S.feito++; } catch (e) { S.erros++; console.warn(` ${o.handle}: ${e.message}`); } if (i % 50 === 0) console.log(` ${i}/${lista.length} ok=${S.feito} erro=${S.erros}`); await sleep(jit()); } S.fase = 'concluido'; console.log(`\nThumbs v2 concluido. ok=${S.feito} erro=${S.erros}`); } window.BAIXA_THUMB = { quantos: async () => { const a = await alvos(); return {total: a.length, jaFeitos: FEITOS.size, restam: a.length - FEITOS.size}; }, iniciar, status: () => ({...S}), parar: () => { S.parar = true; }, retomar: () => iniciar(), limparMarcas: () => localStorage.removeItem('bras_thumbs_v2'), }; (async () => { db = await abrir(); const q = await window.BAIXA_THUMB.quantos(); console.log('BAIXA_THUMB v2 pronto:', q, '\nRode BAIXA_THUMB.iniciar() para comecar.'); })(); })();