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
+397
View File
@@ -0,0 +1,397 @@
# PLANO — Brasiliana Fotográfica → S3
> **Comece por aqui.** Documento de transferência: uma sessão nova consegue
> retomar o trabalho só com este arquivo. Detalhe técnico completo em
> `README.md`.
**Objetivo:** os 13.036 itens da Brasiliana Fotográfica — metadados e imagens
em qualidade de preservação — depositados em `s3://brasiliana`, organizados
como acervo arquivístico (procedência + checksums).
**Autorização:** a Biblioteca Nacional foi consultada. Confirmou que o material
é público e que não tem equipe técnica para fornecer um dump; a coleta por
scraping é o caminho combinado. Ritmo cortês e identificação continuam valendo.
> **Reorganização de pastas em 2026-09-10.** Scripts + estado (catálogo,
> `.sqlite`, `.log`, `.tmp/`, credenciais) mudaram de raiz para `pipeline/`;
> visualizações foram para `web/`; `exemplo-item/` virou `examples/exemplo-item/`;
> `censo.py` virou `archive/censo.py`. Todo comando de terminal abaixo já foi
> atualizado para o novo caminho. Detalhe completo em `README.md` →
> "Estrutura do repositório".
>
> **Mudança de plano em 2026-09-10 (importante, ver 2.6): o achado 2.4 abaixo
> não se sustentou.** Cloudflare passou a bloquear bitstream via curl também
> pra itens sem cache de borda — não só HTML. **Tudo (Fases 2 e 3) roda pelo
> navegador agora, trilha única**, não mais A/B separadas por tipo de nome.
> A Fase 4 (legado) segue via curl normalmente — domínio diferente
> (`objdigital.bn.br`), sem esse bloqueio.
---
## 1. Estado em 2026-09-10
| | |
|---|---|
| Fase 1 (metadados) | ✅ **completa** — 13.059 registros, 0 erros. 1 falso-positivo conhecido (`.../1`, é a página raiz da comunidade, não um item — excluir na contagem final) |
| Catálogo | `pipeline/catalogo.jsonl`, re-exportado depois dos reparos (152 itens com `all_preservation_urls`) |
| Download principal (1 imagem/item) | 🟡 em andamento no navegador |
| Thumbnails | 🟡 em andamento no navegador |
| Extras (itens com múltiplos bitstreams, 115 itens) | ✅ completo |
| Fase 4 (legado objdigital) | 🟡 em andamento, local (`baixador.py --somente-legado`), 2.707 itens com `legacy_url` |
| `subidor.py` | rodando em `pipeline/`, `--vigiar`, roteia preservacao/thumbnail/legado certo |
Não existe mais aba do Brave rodando isso — é uma aba de Chrome comum,
conectada via extensão Claude-in-Chrome. `coletor.js`/`coletor-resume.js`
grava em IndexedDB a cada item; fechar a aba não perde nada, mas o *loop*
para e precisa recolar o script certo pra retomar (ver `tasks/TODO.md`
pros comandos exatos de cada trilha ativa).
---
## 2. Cinco achados que não devem ser redescobertos
Cada um destes custou horas. Nenhum é óbvio, e vários são contraintuitivos.
### 2.1 O sitemap está sete anos desatualizado
`htmlmap`/`sitemap` são de abril/2019 e listam ~5.300 dos 13.036 itens. Enumerar
por eles perde **59% do acervo sem erro aparente** — o crawl termina limpo, pela
metade. Fonte correta: `browse?type=title&rpp=100&offset=N`, percorrido por
comunidade (o que de quebra etiqueta a instituição de origem).
### 2.2 O arquivo de preservação está no DSpace, não no objdigital
O `DC.identifier` aponta para `objdigital.bn.br` e parece ser o master. É o
contrário: o bitstream do DSpace tem 5533×4065 (7,4 MB) contra ~2600×2100
(147 KB) do objdigital. O maior medido: 8679×6484, 34,7 MB — EXIF mostra
Hasselblad H3D II-39MS a 7216×5412.
**Inverter isso arquiva o acervo a um quarto da resolução, e nada avisa.**
O PDF de 71,9 MB do item 14582 no objdigital não é a mesma imagem melhor: é
outro objeto, um álbum de várias páginas.
### 2.3 O Cloudflare bloqueia automação de navegador — não IP, não geografia
Medido na mesma máquina, mesmo IP, mesmo instante:
| Cliente | Páginas HTML |
|---|---|
| Chrome/Brave do usuário | ✅ 200 |
| `fetch()` de dentro da página | ✅ 200 (>100 seguidas) |
| Playwright *headed* | ❌ 200 na 1ª, 403 depois |
| Playwright *headless* | ❌ 403 |
| curl / httpx | ❌ 403 |
| Servidor (Hetzner Helsinki) | ❌ 403 |
**Proxies não ajudam** — o bloqueio dispara na segunda requisição da mesma
sessão, não por origem. Testado e descartado. Não repita.
### 2.4 Bitstreams passam por curl; páginas HTML não
`bitstream/...` responde 200 a `curl -L` simples, até com UA de curl. Só o HTML
é desafiado. É isso que permite baixar os 116 GB fora do navegador, num servidor.
### 2.5 Nome de arquivo com percent-encoding leva 403
Testado com cinco user-agents (Chrome, Safari, Firefox, Googlebot, curl):
**todos 403** numa URL com `%28`/`%20`, enquanto `curl/8.7.1` puro dá **200**
numa URL de nome simples. O gatilho é o caminho, não o cliente — variar
user-agent não resolve. O navegador busca os dois casos sem problema.
Daí as duas trilhas — **modelo original, superado, ver 2.6**. Itens da BN
usam `imagem.jpg` (trilha A); os do IMS usam nomes descritivos com espaços
e acentos (trilha B).
### 2.6 O achado 2.4 não se sustentou — Cloudflare bloqueia bitstream também
Testado ao vivo em 2026-09-10: item nunca antes acessado, buscado pelo
navegador (200 OK) e **imediatamente depois** via curl na Finlândia — 403,
página real do Cloudflare ("Attention Required"). O único item que passava
por curl (`6066`) só passava porque já tinha sido baixado com sucesso antes
e ficou em cache de borda (`cf-cache-status: HIT`) — sobra de um caso, não
regra geral. Pré-aquecer a URL pelo navegador antes de tentar via curl
**não** transfere o cache pra tornar o curl aceito (testado, falha igual).
**Consequência: não existe mais trilha A via servidor.** Tudo — nome simples
ou com encoding — precisa passar pelo navegador vivo (`fetch()` de dentro da
página). `baixador.py` na Finlândia fica só pra Fase 4 (objdigital, domínio
diferente, sem esse bloqueio — ver 2.11).
### 2.7 Itens com múltiplos bitstreams reais — não é caso raro
115 itens (de ~13.058) têm **2 a 6 imagens reais** no bundle ORIGINAL, não
uma. `citation_pdf_url` não é gerado pra esses (o gerador de citação do
DSpace não sabe qual é "a" imagem quando há mais de uma) — por isso ficam
sem `preservation_url` na extração simples. Sintoma: `full_table` tem
`dc.description` mencionando álbum/página (ex.: "Álbum 0259"), igual ao
caso do PDF de 71,9 MB do achado 2.2 — mesmo fenômeno, mas dentro do
próprio DSpace, não só no objdigital.
**Como enumerar as imagens reais de um item**, em ordem de confiabilidade:
1. Contar `.file-wrapper` na página normal do item (não `?show=full`) — bate
com o número real de bitstreams.
2. Tentar sequências `/bitstream/{PREFIX}/{h}/{seq}/qualquer-nome.jpg` pra
`seq` de 1 até N — funciona quando as sequências são contíguas a partir
de 1 (maioria dos casos).
3. Quando (2) falha (sequências não contíguas): pegar o nome real do
arquivo a partir do `<img src>` de cada `.file-wrapper` (é a miniatura,
nome com `.jpg` duplicado no final — ex. `nome.jpg.jpg`), tirar o último
`.jpg`, e buscar `/bitstream/handle/{PREFIX}/{h}/{nome-real}` (rota por
nome exato, não por sequência).
`preservation_url` = a maior imagem encontrada (compatibilidade com o
pipeline); `all_preservation_urls`/`all_preservation_sizes` guardam todas,
ordenadas da maior pra menor.
**Exceção conhecida sem conserto:** handle `20.500.12156.1/7523` — `HTTP
500` do próprio servidor da BN ao buscar os bitstreams (não é problema de
nome/padrão). Provável arquivo corrompido do lado deles.
### 2.8b Achado 2.8 falha ~62% das vezes em alguns lotes — bitstream de LICENÇA, não thumbnail
`seq_master+1` às vezes é o bitstream de **LICENÇA** do DSpace (texto puro,
"LICENÇA DE VEICULAÇÃO E DISTRIBUIÇÃO..."), não a miniatura — visto em
Marinha e IMS, **2.655 de 4.278 thumbnails** (62%!) baixados errado num
lote, **146 já tinham subido pro bucket** antes de descobrir (limpo).
O filtro de tamanho do v1 (`> 2MB → rejeita`) não pega isso: o texto da
licença é pequeno o bastante (781 bytes) pra passar. **Fix:** validar
magic bytes (JPEG `FFD8`/PNG) antes de aceitar, tentar seq+2/+3 se falhar,
e cair pro método do achado 2.7 (`<img src>` da página) como último
recurso — `baixador-thumb.js` v2. Também blindado no `subidor.py`: rejeita
qualquer upload cujo conteúdo não comece com magic bytes de imagem,
independente do nome do arquivo (defesa em camadas).
**Lição:** tamanho pequeno não é prova de "é a miniatura" — só magic bytes
provam que é imagem de verdade.
### 2.8 Miniatura gerada pelo DSpace: sequência N+1 (quando contígua)
Pra item com bitstream original na sequência N, a miniatura que o DSpace
gera automaticamente costuma ficar na sequência N+1 — confirmado no handle
`6066`: seq 1 = master (7,7 MB), seq 2 = thumb (56 KB). Mesma rota
filename-agnostic do achado 2.4 original (o nome na URL não importa, só a
sequência). Quando as sequências não são contíguas (itens do achado 2.7 via
método 3), a miniatura de cada imagem é o próprio `<img src>` do
`.file-wrapper` correspondente — não precisa de request extra.
### 2.9 objdigital às vezes tem PDF (álbum), não só JPG — extração pegava o identificador errado
Alguns itens têm mais de um `DC.identifier` apontando pro objdigital: um
`.htm` (página de landing) e um `.pdf` (o álbum de verdade, múltiplas
páginas — mesmo fenômeno do PDF de 71,9 MB do item 14582, achado 2.2). A
extração original pegava o **primeiro** identificador que batesse com
"objdigital", que pra 44 itens (a maioria uma sequência do Museu
Aeroespacial, `14526-14570`, mais o `584`) era o `.htm`, não o `.pdf` —
perdendo o conteúdo real. De 235 itens com PDF disponível, 191 já estavam
certos por acaso (o PDF vinha primeiro na lista); só 44 precisaram de
correção manual (feita em `catalogo.jsonl` e no IndexedDB, antes do
`baixador.py --somente-legado` alcançar esses handles — nada foi
contaminado no bucket). **Vale re-checar outros itens com múltiplos
`DC.identifier` de objdigital por precaução, não só os que têm `.pdf`.**
Recheque feito (2026-09-10): mapeamos todos os padrões de extensão em
2.231 itens com 2+ identifiers objdigital. Achados adicionais:
- **5 itens** (`1081, 1082, 1084, 4077, 4084`) têm **só HTML**, sem
`.jpg`/`.pdf` nenhum — são páginas de um visualizador de zoom profundo
(OpenSeadragon/DZI, pirâmide de blocos, não um arquivo único). **3 já
tinham sido baixados e subidos como se fossem imagem** (arquivos de
~2,4 KB, só a página HTML do visualizador) — limpo do bucket, `legacy_url`
zerado pros 5 (não vale a complexidade de espelhar a pirâmide de tiles
pra tão poucos itens).
- **11 itens** têm `.sid` (MrSID, formato comprimido de imagem grande)
além do `.jpg` — não capturado, ficou como gap conhecido (baixo volume,
não priorizado).
### 2.10 O crawl da Fase 1 não é 100% completo — achado via referência cruzada com o WordPress
Cruzando os handles citados nos 636 posts do WordPress contra o catálogo,
2 itens reais (`1799` "Princesa Isabel menina", `4798` "Vista panorâmica de
Canudos...") existem no site mas não foram enumerados pela Fase 1 — ambos
sem `citation_pdf_url`, formato de metadado antigo (um é de 2015-04-16,
uma das primeiras entradas do sistema). Adicionados manualmente ao
catálogo depois de descobertos.
**Isso só foi achado por acaso** (porque um post do blog citava esses
handles especificamente) — não existe verificação exaustiva de que a
enumeração por `browse?type=title` capturou 100% dos itens de cada
comunidade. Pode haver mais itens faltando que nenhum post referencia.
Não investigado a fundo: seria preciso comparar a contagem de cada
comunidade contra alguma fonte independente (a BN não expõe OAI-PMH nem
count via API, até onde sabemos).
### 2.11 O objdigital só responde em HTTPS — não era bloqueio
`legacy_url` na fonte vem como `http://objdigital.bn.br/...`. Conexão HTTP
simples (porta 80) é resetada (`Recv failure: Connection reset by peer`),
local e da Finlândia. HTTPS funciona liso com curl puro, local (não
testado da Finlândia — motivo original de "não alcançável" pode ser outro,
não investigado). Fix: forçar `https://` no `legacy_url` antes de baixar
(já aplicado em `baixador.py`).
### 2.12 Caractere `#` no nome do arquivo corrompe URL no IndexedDB
78 itens (mesmo álbum "Album 0035/0259/0260") têm `#` literal no nome real
do arquivo (ex.: `Alb 0260 199 CT #.jpg`). Algum processo que passou esse
nome por `new URL(...).pathname` sofreu o efeito clássico: `#` não
percentado é tratado como início de **fragmento da URL**, e tudo depois
dele é descartado — a URL salva no IndexedDB virou `.../Alb%20...%20CT%20`
(sem sequência, sem `#`, sem `.jpg`), 404 garantido. O `catalogo.jsonl`
local nunca foi tocado por esse bug (tem a URL certa, com `%23`) — fix é
só reescrever o campo no IndexedDB a partir do catálogo
(`pipeline/reparo-hash-url.js`).
**Lição:** nunca usar `new URL()`/`.pathname` pra manipular nomes de
arquivo vindos de fonte externa sem antes confirmar que não têm `#`, `?`
ou outros caracteres especiais de URL não codificados.
### 2.13 O mesmo `#` quebra de novo — desta vez no redirect do próprio DSpace
Mesmo com a URL do achado 2.12 corrigida no IndexedDB (`%23` correto, forma
antiga `/bitstream/PREFIX/ID/SEQ/arquivo`), os 78 itens continuavam dando
404. O DSpace responde a essa forma antiga com um redirect para a forma
canônica nova (`/bitstream/handle/PREFIX/ID/arquivo`, sem número de
sequência) — e o `Location` do redirect tem o `#` sem escapar de novo,
então o `fetch()` do navegador trunca no fragmento ao seguir o redirect
(`finalUrl` chega faltando `#.jpg`, 404). Fix: pedir direto a forma
canônica (`/bitstream/handle/...`), pulando o redirect — testado, `200`,
JPEG válido. `preservation_url` desses 78 itens reescrito no IndexedDB
para a forma canônica.
**Lição:** com `#` no nome do arquivo, não confiar em nenhum redirect do
servidor — ele pode ter o mesmo bug do lado de lá. Preferir sempre a URL
final/canônica quando ela for conhecida.
### 2.14 Dá pra ler o progresso do BAIXA/BAIXA_THUMB sem o Chrome conectado
O progresso das trilhas via navegador (`localStorage.bras_baixados`,
`bras_thumbs_v2`) sobrevive num arquivo leveldb comum no disco —
`~/Library/Application Support/Google/Chrome/Default/Local Storage/leveldb`
— mesmo com a extensão Claude-in-Chrome desconectada (ex.: conflito de conta
OAuth, sessão anterior perdida). Basta copiar o diretório (evita brigar com
o `LOCK` que o Chrome mantém aberto) e ler com uma lib leveldb qualquer
(`classic-level` via npm, sem dependência nativa externa). O valor vem com
1 byte de prefixo indicando encoding (`0x00`=UTF-16LE, `0x01`=UTF-8/Latin1)
antes do JSON de verdade. Script pronto: `pipeline/tools/ler-localstorage.js`.
**Lição:** perder a conexão com a extensão do navegador não significa perder
visibilidade do progresso — boa parte do estado do app mora em arquivos
comuns no disco do usuário, não só "dentro do navegador".
---
## 3. O acervo
DSpace 4.2, handles `20.500.12156.1/*`. **13.058 itens reais** (13.059
descobertos no crawl, menos 1 falso-positivo — `.../1` é a página raiz da
comunidade, não um item). Contagem original por comunidade abaixo era de
13.036; a diferença é ruído de borda (comunidades ganham/perdem item ao
longo do tempo), não erro de coleta.
| Instituição | Itens | | Instituição | Itens |
|---|--:|---|---|--:|
| Instituto Moreira Salles | 5.133 | | Fiocruz | 548 |
| Biblioteca Nacional | 2.726 | | Marinha (DPHDM) | 456 |
| Museu Aeroespacial | 1.143 | | Arquivo Nacional | 383 |
| Museu da República | 906 | | Arq. Geral da Cidade do RJ | 248 |
| Museu Histórico Nacional | 649 | | Leibniz-Institut, Leipzig | 216 |
| IHGB | 206 | | Fund. Joaquim Nabuco | 190 |
| Museu Goeldi | 95 | | FGV CPDOC | 79 |
| IBGE | 58 | | | |
**~139,5 GB projetados** (recalculado em 2026-09-10 com médias reais de
357-474 arquivos já no bucket por categoria, não mais os 6 originais):
preservação+extras ~136,9 GB (13.152 arquivos, 10,7 MB médio), thumbnails
~0,9 GB, legado ~1,5 GB (taxa de sucesso ~75%, muito 404 genuíno),
metadados+WordPress+catálogo <0,2 GB combinados.
---
## 4. Runbook
### Fase 1 — metadados (navegador) ✅ COMPLETA
Cole `pipeline/coletor-resume.js` (pendentes + erros juntos, numa passada só)
numa aba em `https://brasilianafotografica.bn.gov.br/brasiliana/`. Acompanhar
com `await BRAS.status()`. Deixe a aba **ativa na janela dela**: o Chromium
estrangula `setTimeout` em abas de fundo até 1×/min, e o loop passa a
rastejar sem erro nenhum. Ao terminar: `await BRAS.exportar()`.
### Fases 2 e 3 — trilha única via navegador (não tem mais trilha A/servidor)
Ver achado 2.6. Na mesma aba (mesmo IndexedDB `bras`), cole em sequência:
```js
// pipeline/baixador-navegador.js -- imagem principal, todos os itens
BAIXA.quantos(); BAIXA.iniciar();
// pipeline/baixador-thumb.js -- miniaturas
BAIXA_THUMB.quantos(); BAIXA_THUMB.iniciar();
// pipeline/baixador-extras.js -- 2a+ imagem dos itens com multiplos bitstreams
// (rodar reparo-multibitstream.js antes, se ainda nao rodou)
BAIXA_EXTRA.quantos(); BAIXA_EXTRA.iniciar();
```
Em paralelo, em outro terminal:
```bash
cd pipeline
set -a; . .s3env; set +a
python3 subidor.py --vigiar --catalogo catalogo.jsonl
```
Downloads automáticos do Chrome exigem permissão em
`chrome://settings/content/automaticDownloads` (adicionar
`https://brasilianafotografica.bn.gov.br`) — sem isso o download "funciona"
no JS (sem erro) mas nenhum arquivo chega no disco. Vale pro domínio
inteiro, uma vez só.
### Fase 4 — cópias legadas (máquina local)
```bash
cd pipeline
set -a; . .s3env; set +a
python3 baixador.py --catalogo catalogo.jsonl --somente-legado
```
Via curl direto, sem navegador (ver achado 2.11). ~2.707 itens têm
`legacy_url`, ~328 KB em média.
---
## 5. Arquivos e credenciais
Layout do repo (detalhe em `README.md` → "Estrutura do repositório"):
scripts + estado ao vivo em `pipeline/`, visualizações em `web/`, item de
referência em `examples/`, código morto em `archive/`.
| Arquivo | O quê |
|---|---|
| `tasks/PLANO.md` | Este documento |
| `README.md` | Referência técnica completa (raiz do projeto) |
| `pipeline/coletor.js` | Fase 1 — console do navegador, IndexedDB, autoinício |
| `pipeline/baixador.py` | Fases 2 e 4 — curl → S3, um arquivo por vez |
| `pipeline/baixador-navegador.js` | Fase 3 — navegador → pasta Downloads |
| `pipeline/subidor.py` | Fase 3 — Downloads → S3, apaga na hora |
| `archive/censo.py` | Versão Playwright — **não funciona**, mantida pelo parsing |
| `pipeline/.s3env.exemplo` | Modelo; chaves reais em `/Users/polux/Projetos/tocador/.env` |
**S3:** `hel1.your-objectstorage.com`, bucket `brasiliana` (Hetzner).
Credenciais em `/Users/polux/Projetos/tocador/.env`
(`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). Cópia local em
`pipeline/.s3env`. Na Finlândia: `/root/.s3env`.
**Servidor:** `ssh finland` → 89.167.95.136 (Hetzner Helsinki, root, 31 GB livres).
---
## 6. Estrutura no bucket
```
s3://brasiliana/
MANIFEST.md o que é, como foi coletado, quando
catalogo.jsonl uma linha por item
itens/20.500.12156.1_6066/
metadata.json Dublin Core + URL de origem + data UTC
preservacao/imagem.jpg 5533×4065, 7,4 MB
legado/icon843660.jpg 147 KB, referência de origem
manifest-sha256.txt checksum de cada arquivo
```
Não negociável: **SHA-256 de tudo** e **URL de origem + data/hora de captura**.
Sem os dois é pasta de imagens, não arquivo.
---
## 7. Pendências
1. Terminar a Fase 1 e exportar o catálogo.
2. Medir a fração da trilha B (`BAIXA.quantos()`) — de graça, sem requisição.
3. Confirmar a projeção de ~116 GB (hoje: 6 arquivos medidos).
4. Decidir sobre os PDFs de álbum do objdigital — objetos distintos, o de
71,9 MB provavelmente não é caso isolado.
5. Gerar `MANIFEST.md` e `catalogo.jsonl` no bucket ao final.
## 8. Direitos
Majoritariamente domínio público por antiguidade, mas as 15 instituições têm
políticas distintas. `dc.rights` e a instituição de origem são preservados
**por item**: a decisão de redistribuir é item a item, não do acervo em bloco.
Arquivar para si é uma coisa; republicar é outra.
+611
View File
@@ -0,0 +1,611 @@
# TODO — Brasiliana → S3
> **Handoff em 2026-09-10 ~15h UTC**: sessão `fix-image-pipeline-extras` perdeu
> conexão com a extensão Claude-in-Chrome e encerrou a pedido do usuário.
> Trabalho passado pra sessão `chrome-extension-connection` (tem o navegador
> conectado). Ver mensagem de handoff enviada via SendMessage pro contexto
> completo do estado no momento da troca — resumo abaixo já reflete isso.
>
> **Atualização ~17h UTC (mesma sessão)**: aba antiga do coletor não é
> acessível por esta sessão (grupo de abas é por sessão) mas continuou
> rodando sozinha o tempo todo — confirmado por progresso do
> `bras_baixados` no localStorage (compartilhado por origem). Por isso o
> download principal (`BAIXA`) **não foi reiniciado nesta aba** (evitar
> duplicar ~8 mil downloads pesados em paralelo); só os 78 itens do achado
> 2.11 foram baixados cirurgicamente aqui. `BAIXA_THUMB` v2 foi iniciado
> numa aba nova (chave `bras_thumbs_v2`, começa do zero, ~13.059 itens).
> Durante a sessão houve uma queda de rede/extensão de alguns minutos
> (mesmo tipo de falha que derrubou a sessão anterior) — gerou ~1.280 erros
> transitórios no thumb v2 que precisam de uma segunda passada
> (`BAIXA_THUMB.retomar()`) depois que a primeira terminar.
> Ver `tasks/PLANO.md` pro histórico/achados/runbook. Itens concluídos saem
> daqui e vão pra `tasks/done/` (ex.: `tasks/done/TODO-2026-09-10.md`).
> Scripts e estado em `pipeline/` (reorganização de 2026-09-10).
## Em andamento (não mexer, só monitorar)
- [x] **Upload pro S3 — CONCLUÍDO.** A fila de `~/Downloads` está em **0**; tudo
que foi baixado está no bucket. Total no ledger: **41.444 objetos**.
O `pipeline/vigia-30min.sh` segue de pé: a cada 30 min sobe o que aparecer
e apaga do disco só depois do HEAD confirmar no bucket.
**Não editar `vigia-30min.sh` nem `rodar-paralelo.sh` enquanto rodam**
(o bash lê o script aos poucos) e **não relançar o `subidor.py` antigo**
(achado 2.22). Threads: 6, não 16 — ver achado 2.24.
- [ ] **Download principal** (1 imagem de preservação por item) — navegador. **2026-09-11 20:45 UTC**: **12.986/13.059 (99,44%), faltam 73** — medido pela fonte autoritativa (bucket ∪ fila local), não pelo `bras_baixados`. Parado há ~9h (último arquivo em `~/Downloads` às 13:19 local = 11:19 UTC). Lista exata dos 73 em **`pipeline/faltantes-preservacao.jsonl`** (`{handle, url}`). **Script `pipeline/baixar-faltantes-73.js` colado e rodado pelo usuário ~22:52 local (20:52 UTC)** — lista embutida, chave própria `bras_faltantes_73`, imune ao achado 2.16. **Resultado parcial: 2/73** (`10993`, `10995`, 31 MB). Os outros 71 estão falhando — ver achado 2.17. **Recontagem de 12/09: faltam 211, não 73 — ver achado 2.23.**
### Achado 2.17 — os 71 restantes falham, e não é malformação de URL (2026-09-11 21:25 UTC)
`bras_faltantes_73` = 2 e só 2 arquivos de preservação novos apareceram em
`~/Downloads` (22:52 local). O disco e a chave concordam — aqui o achado 2.16
não se aplica, o número é real.
Os 71 que falham têm URL estruturalmente idêntica às 2 que passaram
(mesmo host, mesmo `/brasiliana/bitstream/PREFIX/ID/SEQ/arquivo`, 21 dos 71 com
`%20` no nome, igual às que funcionaram). Diferenças observadas, nenhuma
conclusiva: as 2 que passaram são `SEQ=1`; entre as 71 há `SEQ` 1 (58), 4 (11),
3 (1) e 5 (1).
Hipótese principal: **as 2 que passaram estavam em cache de borda do Cloudflare**
— exatamente o caso do item 6066 descrito no cabeçalho do
`baixador-navegador.js` (`cf-cache-status: HIT`), e não a regra geral. Se for
isso, o `fetch` de aba não basta para estes 71 e o problema é de origem, não de
script.
**Próximo passo (precisa do console da aba)**: rodar `FALTA.status()` e
`FALTA.erros()` — o script guarda `{handle, url, erro}` de cada falha, e o
código HTTP decide o caminho:
- **404** → bitstream não existe nesse `SEQ`; reparar `preservation_url` no
catálogo (mesma família dos achados 2.9/2.11/2.13) e regerar o script.
- **403** → bloqueio de origem/Cloudflare; nem a aba resolve, precisa de outra saída.
- **500** → transitório; `FALTA.retomar()` mais tarde deve pegar.
Enquanto isso o script pode ainda estar rodando: cada erro dorme 60 s de backoff,
então 71 falhas levam ~71 min a partir das 22:52 local (fim previsto ~00:05).
### Achado 2.18 — é recusa da origem, não URL errada; e a v1 do script ajudou a causar (2026-09-12)
`FALTA.status()` do usuário: `atual: "43/73"`, `erros: 101`, `feito: 0`,
`fase: "baixando"`.
Três leituras, em ordem de importância:
1. **`feito: 0` no item 43** — `10993` e `10995` são os itens 1 e 2 da lista e
baixaram com sucesso às 22:52; nesta passada **falharam**. Mesma URL, uma hora
depois. Logo **não é URL errada, é a origem recusando**.
2. **`erros: 101` com `atual: 43/73`** — mais erros que itens tentados só é
possível com **dois ou mais loops concorrentes** somando no mesmo `S`. Cada
`iniciar()`/`retomar()` sobre um script já rodando cria outro loop. Isso
quebrou a pausa de 3 s combinada com a BN e provavelmente disparou rate-limit.
3. `total: 73` (e não 71) — o `FEITOS` foi lido vazio na largada dessa passada.
**Hipóteses mortas** (não perder tempo com elas de novo):
- *Sequência errada no `preservation_url`*: os 2 que funcionaram têm exatamente
a mesma forma dos 13 "divergentes" (catálogo `seq=1`, thumb em `seq=3`), e 58
dos 71 têm `seq` coerente com o thumb e falham igual.
- *Bloqueio geral do Cloudflare*: **71/71 desses itens têm thumb baixado com
sucesso** — o item responde; só o bitstream de preservação não.
**`pipeline/baixar-faltantes-v2.js`** corrige os defeitos da v1 (que eram meus):
guarda contra `iniciar()` duplo, **aborta depois de 5 erros seguidos** em vez de
moer os 71 com 60 s cada, pausa de 5 s, backoff de 30 s, e registra o código HTTP
separado (`FALTA.resumo()` devolve a tabela de códigos). Recarregar a aba antes
de colar — `parar()` não alcança closures de colagens anteriores.
- [x] **Thumbnails** — ✅ concluído 100% (13.059/13.059). `retomar()` já foi rodado uma vez (08h local); **não re-rodar**.
- [x] **Fase 4 (legado objdigital)** — ✅ concluído (2ª passada terminou): 2.137 ok / 565 erro (2.702/2.707 processados; os 565 são 404 permanentes confirmados via curl direto, atrito real da fonte legada, não bug). Confirmado em 2026-09-11 ~20h UTC: `select erro,count(*)` devolve **só `HTTP 404` (565)** — nenhum `HTTP 0` restante, então a 3ª passada não tem o que reprocessar; **não reiniciar de novo**.
- [x] ~~`subidor.py`~~ **substituído pelo `subidor-paralelo.py` em 2026-09-12 (achado 2.22); o texto a seguir é histórico.** (`pipeline/`, `--vigiar`, PID 97295) — vivo há 1d8h, vigiando `~/Downloads`. **2026-09-11 ~20h UTC**: fila local de **19.897 arquivos `BRAS__` / ~77 GB** (12.511 thumb, 1.660 `__imagem`, 5.726 com nome original). Bucket: 33.388 objetos / 60,3 GB, 8.080 itens com `metadata.json` (61,9% dos 13.059). **2026-09-12 09:45 UTC**: 16.207 enviados, fila de 18.286. Taxa nas horas acordadas ≈ **650–740 arq/h ≈ 2,0–2,4 GB/h**.
⚠️ **O relógio é de horas ACORDADAS, não de horas corridas.** Os envios pararam
às 22:30 UTC de 11/set e só voltaram às 09:00 UTC de 12/set — **10,5 h de
silêncio**, que é a máquina dormindo (00:30–11:00 local), não falha do subidor
(o processo seguiu vivo e voltou sozinho). Por isso:
- fila de 18.286 ÷ ~700 arq/h = **~26 h de máquina acordada**
- em dias normais (~14 h acordado), isso vira **~2 dias de calendário**
- com `caffeinate -is` segurando a máquina acordada, ~26 h corridas
Vale decidir isso conscientemente: é a diferença entre terminar em ~1 dia ou ~2.
### Achado 2.15 — aba do coletor inalcançável, e storage da origem aparece vazio (2026-09-11 ~20h UTC)
Tentativa de retomar a trilha 1 sozinho falhou, mas rendeu dois fatos novos:
1. Sobraram instâncias **headless do Chrome** de uma sessão anterior, uma delas
lançada com o **perfil padrão** (`--user-data-dir` default). Mortas (PIDs
89329, 89327, 14818); o Chrome real do usuário (PID 57263, aberto desde
9/set) foi preservado. **Nunca subir headless sem `--user-data-dir` próprio**
— disputa o perfil onde mora todo o estado do coletor.
2. Numa aba nova em `https://brasilianafotografica.bn.gov.br` (mesmo perfil,
mesma origem), `localStorage` devolve **0 chaves** e `indexedDB.databases()`
devolve **[]** — enquanto `ler-localstorage.js`, lendo o leveldb do disco do
mesmo perfil/origem, devolve `bras_baixados: 12.974`. Ou seja: o contexto vivo
e o disco discordam. Não dá pra `BAIXA.retomar()` numa aba nova: sem o
IndexedDB `bras`, `alvos()` devolveria `[]`.
**Não colar `baixador-navegador.js` numa aba nesse estado**: se o storage
estiver mesmo zerado, `FEITOS` começa vazio e a fila vira os ~13 mil itens
inteiros de novo (centenas de GB, e carga indevida na fonte). O caminho seguro
é o usuário voltar na **aba original do coletor** (a que já tem `BAIXA` no
`window`) e rodar `BAIXA.status()` / `BAIXA.retomar()` lá.
### Achado 2.16 — `bras_baixados` anda PRA TRÁS; não usar como medida (2026-09-11 20:45 UTC)
O contador caiu de **12.974 → 12.111** entre 20:14 e 20:43 UTC (três leituras
seguidas devolvem 12.111; o leveldb foi escrito pelo Chrome às 22:24 local, 19
min antes da leitura — é gravação real, não cache velho).
Causa provável: `marcar()` reescreve o **array inteiro** a partir do `FEITOS`
carregado quando o script foi colado. Qualquer aba com snapshot velho que ainda
marque um item sobrescreve a lista boa por uma mais curta. Com duas abas vivas
(v1 e v2 conviveram), a lista é sempre a do último escritor, não a união.
**Consequência prática**: `bras_baixados` **não** mede progresso — subestima. A
medida autoritativa é *bucket ∪ fila local em `~/Downloads`*:
```
# handles com preservação no bucket (do aws s3 ls --recursive)
awk '... itens/<handle>/<arquivo!=metadata.json, sem "thumb">' → 8.080
# handles de preservação ainda na fila local
ls ~/Downloads | grep '^BRAS__' | grep -v thumb | sed -E 's/^BRAS__(.+)__.*/\1/' → 7.271
# união → 12.986 de 13.059 (faltam 73, listados em pipeline/faltantes-preservacao.jsonl)
```
Isso também refaz a conta do achado 2.15: o risco de recolar o script não é
"85 itens", é **re-baixar ~950+** que o contador esqueceu (ou tudo, se o
storage estiver mesmo zerado).
## Concluído agora
- [x] **Thumbnails contaminados com bitstream de LICENÇA** (achado 2.8b): 2.653 arquivos locais apagados de `~/Downloads`, 146 já subidos removidos do bucket, `subidor.py` blindado com validação de magic bytes (rejeita e apaga automaticamente qualquer coisa que não comece com JPEG/PNG, mesmo que o nome diga o contrário). `baixador-thumb.js` v2 escrito com validação + fallback — falta só reconectar o navegador pra trocar o script rodando.
- [x] **WordPress → .md**: 636/637 posts colhidos (1 erro), exportados e convertidos — `wordpress/{id}-{slug}.md` no repo (front-matter YAML: id/url/título/autor/data/categorias/handles referenciados) + `wordpress-posts.jsonl` e a pasta `wordpress/` subindo pro bucket agora.
- [x] **Bug de corrida em arquivo temporário**: `baixador.py` e `subidor.py` moram na mesma pasta e usavam o mesmo nome fixo (`.tmp/metadata.json`) — rodando ao mesmo tempo, um apagava o arquivo que o outro tinha acabado de criar. Derrubou o `baixador.py` de verdade (crash, não só log de erro). Corrigido: nome agora inclui o PID. 3 itens (`1081,1082,1084`) já tinham subido um HTML de visualizador como se fosse "legado" antes da correção — limpo do bucket, metadata regenerado.
- [x] **PDFs de álbum perdidos no objdigital**: 44 itens (achado 2.9 do PLANO.md) tinham PDF disponível mas a extração pegava um `.htm` de landing page em vez do PDF de verdade. Corrigido em `catalogo.jsonl` e no IndexedDB antes do `baixador.py --somente-legado` alcançar esses handles (confirmado: nada contaminado no bucket). `baixador.py` reiniciado (PID 37909) com o catálogo corrigido.
## Concluído nesta sessão (chrome-extension-connection, ~17h UTC)
- [x] `pipeline/reparo-hash-url.js` colado e rodado — 78/78 corrigidos no IndexedDB
- [x] **Achado 2.13 (novo)**: mesmo bug do `#` reaparecia no redirect do DSpace (forma antiga → canônica). Fix: usar `/bitstream/handle/PREFIX/ID/arquivo` direto. `preservation_url` dos 78 reescrito de novo com a forma canônica.
- [x] Os 78 itens baixados cirurgicamente — **concluído 78/78, 0 erros** (script à parte, não o `BAIXA.retomar()` geral — aba antiga com o loop principal continua rodando sozinha, ver nota de handoff acima)
- [x] `BAIXA_THUMB` v2 iniciado (aba nova, não foi possível parar a v1 na aba antiga — ver nota acima; sem risco de dado ruim no bucket graças à validação de magic bytes já no `subidor.py`)
- [x] **Thumbnails v2: CONCLUÍDO 100%** — `BAIXA_THUMB.retomar()` rodado (2026-09-11 ~08h local) recuperou todos os 1.281 erros da queda de rede (13.059/13.059 ok, 0 erro permanente)
- [x] `baixador.py --somente-legado` reiniciado uma vez (2026-09-11 ~08h local, PID 88387) depois que a passada anterior terminou (2.113 ok, 589 erro de 2.702 processados) — idempotente, só reprocessa não-'ok'
- [x] Checagem de status a cada 30 min retomada nesta sessão (ver `/loop`)
## Mural (`web/wall.html`) — sessão `fix-mobile-wall-click-layout`, 2026-09-12
No ar em <https://xn--2dk.xyz/brasiliana/>. Commits em `rafapolo/xyz`:
`d8c3364` e `a6085fa` (empurrados pela sessão paralela `brasiliano-ae`, contêm
trabalho das duas) e `f645008`. O mural é publicado copiando
`web/wall.html` → `xyz/brasiliana/index.html` e `web/atlas/atlas-map.json` →
`xyz/brasiliana/atlas/atlas-map.json`.
### Achado 3.1 — a metade descritiva do catálogo nunca chegou ao mural
Buscar "indigenista" não achava nada, mesmo com 42 peças classificadas
`dc.subject = Política indigenista`. A busca livre estava certa (varre todo o
`meta` recursivamente); quem descartava era o **`summarize()` do
`web/build-atlas.js`**, que projetava 8 campos e jogava fora assunto,
descrição, lugar, fonte, editor, relação e código de arquivo.
Havia um segundo bug atrás: `pickDC()` devolve só a **primeira** ocorrência do
campo. Como o catálogo repete `dc.subject` (78.835 linhas para 13.060
registros, ~6 por item), mesmo incluindo o campo sobreviveria um assunto por
peça. O `pickAll()` novo coleta todas, deduplicadas, na ordem.
Regras que ficaram (espelhadas em `pipeline/reprojeta-meta-mapa.mjs`):
- entram: `dc.subject`, `dc.description`, `dc.coverage`, `dc.source`,
`dc.publisher`, `dc.relation`, `dc.identifier`
- ficam de fora: `dc.language` (é sempre `"por"` — casaria ~2.000 itens por
acidente, já que a busca é por substring) e `dc.format` (`"JPG"`)
- `dc.identifier` e `dc.relation` entram **sem as URLs**: era 1,1 MB de texto
que ninguém busca, escondendo o que presta — os códigos de arquivo
(`013RJ012036`) e os nomes de coleção (`Gilberto Ferrez`, `Diários
Associados`)
- `handle` e `source_url` **saíram** do `meta`: o primeiro é a chave sob a qual
o item já está arquivado no mapa, o segundo é o mesmo endereço para todos com
o handle no meio. O mural remonta a URL numa linha (`sourceUrl()`) e injeta o
handle na palha da busca, então buscar `5546` continua achando a peça.
Verificado no navegador: `indigenista` → 42, `Gilberto Ferrez` → 2.120,
`013RJ012036` → 2, `amazônia` → 156, `5546` → 1. E o ruído sumiu: `show=full`
e `brasilianafotografica` saíram de 12.552 para 0.
### Achado 3.2 — não vale pré-comprimir o `atlas-map.json`
Medido, não chutado:
- **No fio já vem gzipado.** `curl -sI` em <https://xn--2dk.xyz/brasiliana/atlas/atlas-map.json>
responde `content-encoding: gzip`. Pré-comprimir só moveria os mesmos bytes,
ao custo de um `DecompressionStream` no carregador e de um arquivo ilegível.
(O host não oferece brotli, e `DecompressionStream` não decodifica brotli.)
- **No repositório também.** O git comprime todo blob com zlib no pack, então
um JSON de 8 MB não ocupa 8 MB lá dentro — ocupa aproximadamente o que o
`.gz` ocuparia, e ainda dá diff.
Encolher o **conteúdo** foi o que valeu, e só até certo ponto — o gzip come
quase todo o ganho estrutural:
| variante | cru | gzip |
|---|---|---|
| com os campos descritivos | 9,61 MB | 1,11 MB |
| **sem `source_url` e `meta.handle`** (feito) | **8,00 MB** | **1,05 MB** |
| + dicionário nos campos de vocabulário pequeno | 6,96 MB | 0,99 MB |
| + dicionário de assuntos/lugares/descrições | 5,14 MB | 0,93 MB |
As duas últimas linhas quase dobram a economia crua e rendem 30-60 KB no fio
cada, em troca de uma camada de dicionário no carregador — troca ruim. Onde o
tamanho cru ainda pesa é o `JSON.parse` no celular, não o download. Referência:
antes de tudo isso o mapa era 5,52 MB / 0,73 MB.
### Consertos de interação (todos verificados por CDP num Chrome real)
- **Toque numa peça abria e fechava a ficha no mesmo gesto** (metade de cima da
tela). Um toque dispara `pointerup` e depois um `click` de compatibilidade,
que caía no backdrop recém-aparecido; a guarda de 400 ms usava
`stopPropagation()`, que **não alcança listeners do mesmo nó** — e quem fecha
a ficha é um listener do próprio `#card-wrap`. Agora é
`stopImmediatePropagation()`.
- **Tela inicial "sem estilo"**: `#title-screen` era irmão flex do `<p>` e,
invisível, ainda tomava metade da linha; e a regra `#loading p` pegava também
o subtítulo, jogando-o em maiúsculas com tracking. Virou `position:absolute` e
`#loading-msg`.
- **Painel de filtros fugia do dedo** no arrasto da década: o chip do filtro
aparecia e inseria uma linha inteira acima do corpo; o badge de contagem
crescia o header. Chips agora só com o painel fechado, badge reserva a caixa
com `visibility`.
- **Movimento da parede**: a câmera mirava o *alvo* enquanto estava atrás dele
(inclinava a vista a cada arrasto — era isso que fazia a parede "nadar"); o
easing era fração por frame (a 120 Hz convergia 2× mais rápido que a 60).
Agora constante de tempo, `lookAt` removido, e soltar em movimento joga a
parede com desaceleração. Zoom do scroll virou multiplicativo.
- **Back/forward do navegador** andam pelas peças abertas, com `#handle` na URL
(link direto reabre a peça). Mudar um filtro recolhe a ficha com **um** passo
de histórico, mesmo com `apply()` rodando a cada frame do slider.
- **Filtros alcançáveis com a peça aberta**: o painel estava em `z-index: 15`
debaixo do `#card-wrap` (20). Agora 25 (scrim 24), e no celular a ficha
estaciona acima do peek da sheet para não cobrir ‹ anterior / próxima ›.
## Miniaturas derivadas + reconciliação do ledger — 2026-09-12
Mural no ar com **13.059 peças** (antes 12.552): commits `bba32f6`,
`6a82a5e`, `1ecded1` e `e930158` em `rafapolo/xyz`. Desta vez o `atlas/` inteiro foi junto, não só o
`atlas-map.json` — o formato do map mudou (ver achado 3.4).
**Números fechados em 13/09 (medidos contra catálogo × ledger):**
| | |
|---|--:|
| itens no catálogo | 13.060 |
| com imagem de preservação | 12.985 |
| com thumbnail do DSpace | 13.059 |
| com miniatura derivada (`derivados/thumb256/`) | 13.059 |
| **no mural** | **13.059** |
| fora do mural | **1** — só `7523` |
Os 8 que faltavam (`7802`, `7814`, `7817`, `7848`, `7852`, `7861`, `7875`,
`8075`) entraram: eles **têm** `thumbnail/thumb_3.jpg` de imagem real no
bucket (109–250 KB). A afirmação anterior de que o thumbnail deles era texto
de licença estava errada — o texto era o `thumb_2` do material antigo do
finland, não o objeto do bucket.
Os 73 faltantes de preservação (achado 2.17/2.18) **não** ficam de fora do
mural: têm thumbnail do DSpace, que basta para o tile de 128px. Faltar
preservação e faltar no mural são coisas diferentes.
- [x] **Miniaturas derivadas — 13.051 itens (99,93%)**
em `s3://brasiliana/derivados/thumb256/` (256px WebP q80, ~8 KB cada,
100 MB no total). Prefixo `derivados/` deliberadamente fora de `itens/`:
`itens/` é material de origem, `derivados/` é reconstruível do zero.
Misturar os dois apagaria a procedência que o MANIFEST garante.
Composição final (medida, igual à do MANIFEST): **12.849 derivadas da
imagem de preservação** + **202 derivadas do `thumbnail/` do DSpace**
(itens com miniatura mas sem preservação coletada). Caminho até lá:
6.338 no finland a partir do bucket, 6.483 locais a partir de
`~/Downloads`, 164 do DSpace, e +66 do material do achado 2.21.
- [x] **MANIFEST.md atualizado e subido** — seção `derivados/`, os achados
2.19 e 2.20 abaixo, e a nota de que `thumbnail/` do DSpace não é
miniatura (achado 3.3). Re-subido em 12/09 com 13.051 / 99,93%, os 9
ausentes nomeados e a divergência 13.058 × 13.060 anotada.
- [x] **`build-atlas.js` parou de descartar item sem título** — a guarda
`if (!o.title) continue` escondia do mural `11749` (Museu Aeroespacial) e
`6241` (Instituto Moreira Salles), que têm imagem; o card já cai pra
"(sem título)" sozinho. 12.983 → 12.985.
- [x] **Wordmark "Acervo Brasiliana" ganhou fundo preto** — só tinha
`text-shadow`, que some sobre peça clara, e peça clara é metade do
acervo. Mesmo preto da dica do rodapé.
- [x] **Rótulo de contagem deixou de dizer "no acervo"** — dizia
"N peças no acervo" como se o mural fosse o acervo inteiro; agora diz
"peças com imagem". Foi essa frase que levantou a dúvida de que faltavam
miniaturas.
- [x] **`web/build-atlas.js` ganhou `--thumbs=DIR`** — a fonte antiga
(`~/Downloads`) é varrida pelo `subidor.py` conforme sobe, então nunca
tem o acervo inteiro ao mesmo tempo. Agora a fonte é reconstruível do
bucket.
- [x] **Ledger `subidor.sqlite` reconciliado com o bucket**: 28.553 linhas =
28.553 objetos, zero divergência nos dois sentidos. Backups em
`/private/tmp/claude-501/.../scratchpad/subidor.sqlite.bak-*`.
- 145 linhas de `thumbnail` sem objeto no bucket — são os bitstreams de
LICENÇA do achado 2.8b, removidos do bucket mas deixados no ledger
(426–815 bytes, só **5 sha256 distintos** entre os 145). Apagadas.
- 3 objetos no bucket sem linha nenhuma — inseridos, sha256 recalculado
do próprio objeto.
- 2.137 objetos `legado/` que **nenhum** ledger de upload conhecia: o
`baixador.py` só anota no `baixador.sqlite`. Importados, para que
`subidor.sqlite` seja ledger único.
- 13.051 derivados registrados como `tipo='derivado-thumb256'`.
- 63 "órfãos" de preservação/thumbnail eram **falso positivo**: gravados
durante a própria listagem do bucket. Conferidos no S3, estão lá.
### Achado 2.24 — 16 threads estouram o OpenSSL do macOS (SIGSEGV)
O `subidor-paralelo.py` morreu com **segmentation fault** no arranque da
passada das 10:56 de 13/09, e por isso **nada subiu durante horas** (o
`vigia-30min` só tentaria de novo meia hora depois). O relatório do macOS
aponta a pilha dentro do `libcrypto`:
```
libcrypto.3.dylib ossl_x509_store_ctx_get_by_subject
libcrypto.3.dylib X509_STORE_CTX_get1_issuer
libssl.3.dylib ssl_verify_internal / tls_post_process_server_certificate
```
É corrida de thread na verificação de certificado TLS quando muitas threads
fazem handshake ao mesmo tempo — bug do OpenSSL, não do nosso código. Com 4
threads subiu 50 arquivos a 19 GB/h sem problema. **Default baixado para 6
threads** no `subidor-paralelo.py`, no `vigia-30min.sh` e no
`rodar-paralelo.sh`. Não vale voltar a subir: acima de ~8 o ganho é nulo (o
link satura) e o risco de crash volta.
### Achado 2.25 — 75 itens com HTTP 500 persistente na origem (não é só o 7523)
Passada de 13/09 com a aba conectada: **108 imagens novas baixaram**, e **74
falharam com erro que não passa** — 45 × HTTP 500, 26 de rede e 3 de blob
curto, com retry sem recuperar nenhuma. Somando o `7523`, são **75 itens**
cujos bitstreams de preservação a origem não serve. Lista em
`pipeline/falhas-500-2026-09-13.txt`.
Isso substitui o achado 2.23 (que falava em 211 faltantes): os outros 136
baixaram. Nenhum dos 75 fica fora do mural — todos têm thumbnail do DSpace.
Diagnóstico que vale registrar: os 500 são **do item, não do ritmo**. Com 3
requisições simultâneas, `6545` e `6555` davam 500 enquanto `1014`, `8128`,
`10039`, `14358` e `6520` davam 200 no mesmo instante, inclusive em série com
3 s de espaçamento. E os itens ruins são vizinhos de id, então em ordem
crescente eles davam erros seguidos e disparavam o aborto automático — por
isso o motor passou a **embaralhar** a lista.
### Achado 2.22 — o gargalo do upload era o `subidor.py`, não o link; troca pelo paralelo
Eu tinha escrito aqui que o gargalo era o link de casa. **Errado.** Medido em
12/09 (`networkQuality`): **69 Mbps de upload (~31 GB/h)**, contra ~1,3 GB/h
reais. O `subidor.py` sobe um arquivo por vez, espera 1,5 s de "estabilidade" e
dispara **três** `aws s3 cp` por arquivo (arquivo, `metadata.json`, manifest),
cada um pagando a partida do aws-cli: ~5 s por arquivo, seja miniatura de 74 KB
ou preservação de 40 MB.
Além de lento, ele estava estragando duas coisas:
- **873 manifests com linha errada**, `derivado-thumb256/<handle>.webp`. É
consequência de eu ter registrado os derivados no ledger: ele lista toda
linha do handle, e esse caminho não existe em `itens/<handle>/`.
- **Duplicatas `(N)` voltando ao bucket** depois da limpeza do achado 2.20
(ex.: `5304`, `thumb_2.jpg` e `thumb_2 (1).jpg` com o mesmo sha256).
Substituto: `pipeline/subidor-paralelo.py` (boto3, threads, HEAD antes de apagar
o local, manifest uma vez por item, sem linha de derivado, duplicata e rejeito
registrados) + `pipeline/rodar-paralelo.sh`. Teste com boto3 contra o bucket:
PUT de 20 MB a 27 Mbps num único stream.
Testes antes de soltar, com o que cada um mostrou:
1. Lote de 30: todos eram rebaixa já presente no bucket e foram **apagados do
disco sem HEAD e sem registro**. Foi o que motivou a conferência por HEAD e a
tabela `descartados`. Para esses 30, a garantia é só o ledger, reconciliado
com o bucket às 12:50.
2. Lote de 150: 150 descartes, 150 conferidos no bucket.
3. Lote de 800: 319 enviados (319 conferidos, `image/jpeg`) + 481 descartes
conferidos; manifests dos 319 sem linha de derivado, sha do metadata batendo.
4. Primeira execução real (18:4x UTC) **interrompida**: o log mostrava TIFF
indo para `rejeitados/` (achado 2.19, correção). Assinaturas corrigidas; os 21
da quarentena (19 TIFF + 2 BMP) voltaram para a fila; relançado às 18:59.
Antes do relançamento, **180 arquivos locais (0,25 GB) já presentes no S3** foram
apagados do disco a pedido, cada um com MD5 local = ETag do objeto e registro em
`descartados`. Os outros 13.922 não estavam no bucket.
### Achado 2.23 — faltam 211 preservações depois da fila, não 73
Cruzando catálogo × (ledger ∪ fila) em 12/09: **12.849 / 13.060** itens terão
imagem de preservação quando a fila acabar (thumbnail: 13.059). Os 211:
- **71** da lista conhecida (`faltantes-preservacao.jsonl`, recusa da origem);
- **1** sem `preservation_url` no catálogo;
- **139** fora de qualquer lista. Exemplo: `10039`–`10049`, que no bucket têm só
`thumbnail/` e `metadata.json`. Desses: 8 rejeitados no upload antigo, 12 com só
`legado/`, ~119 sem rastro (nem bucket, nem fila, nem log).
Hipóteses, nenhuma confirmada: (a) o "12.986/13.059" do achado 2.16 contou
objetos `legado/` e `manifest` como preservação, porque o `awk` só excluía
`metadata.json` e thumbnail; (b) parte dos ~119 pode ser TIFF apagado pelo
`subidor.py` antigo antes do trecho que o `subidor.log` cobre.
### Achado 2.21 — a sessão de 11/09 já tinha as miniaturas no finland, e eu não olhei
`/root/brasiliana-wall/` no finland tinha, desde 11/09 13:17–13:41:
`downloads-flat/` (20.411 arquivos, todos `THUMB__`, 2,2 GB), `s3-thumbs/`
(5.168 handles do bucket, 851 MB) e o `web/atlas/` cujos quatro `.webp` batem
byte a byte com o que estava publicado no `xyz` até 12/09. **O mural de 12.552
peças foi construído lá**, a partir das miniaturas do DSpace — não de
`~/Downloads`, como o comentário do `build-atlas.js` leva a crer.
A união desses dois diretórios cobre **13.041 handles**, praticamente todos
JPEG válido. Eu puxei ~63 GB de imagens de preservação do bucket para derivar
12.985 miniaturas sem ver que 3 GB de miniatura já estavam no disco da mesma
máquina, cobrindo mais itens que o meu resultado. Aproveitados depois: +66
peças (74 no delta, 8 eram licença).
**Lição de runbook**: antes de gerar qualquer coisa no finland, `ls /root` e
`cat /root/.bash_history`. Sessões anteriores deixam material lá e nada no
projeto local registra isso.
Limpeza feita em 12/09: `/root/brasiliana-wall` e `/root/thumbs` apagados
(3,4 GB; disco 27 → 31 GB livres), depois de verificar que os 13.041 handles
estavam **todos** cobertos pelo bucket (7.038) ou pela fila do Mac (6.785) —
zero cópias exclusivas. Ficaram de pé, de propósito: `/root/.s3env` (600, de
08/09, anterior às nossas sessões — pode ser setup permanente da máquina) e
`/root/brasiliana/` (44 MB, `baixador.py` + catálogo + sqlite com 13 linhas).
### Achado 2.19 — a contaminação de LICENÇA também atinge a preservação, não só a miniatura
O achado 2.8b tratou isso como problema da trilha de thumbnail. Não é: os
handles **`20.500.12156.1/6656`** e **`20.500.12156.1/7939`** têm, em
`~/Downloads`, um "imagem.jpg" que é na verdade o bitstream de LICENÇA do
DSpace — 777 bytes de texto UTF-8 com CRLF e extensão `.jpg`. A validação de
magic bytes do `subidor.py` barra no upload, então **nada disso chegou ao
bucket**; o efeito é que esses dois itens não têm imagem coletada e ninguém
percebeu, porque a fila local os contava como prontos.
Implicação: qualquer contagem de "preservação pronta" que leia `~/Downloads`
pelo nome do arquivo está 2 itens otimista. A contagem pelo bucket está certa.
**Atualização 12/09**: não são 2, são 10. Ao derivar as miniaturas do material
do finland (achado 2.21) apareceram mais **oito** com o mesmo defeito, todos
na trilha de thumbnail: `7802`, `7814`, `7817`, `7848`, `7852`, `7861`,
`7875`, `8075`. São exatamente os itens que sobram fora do mural. O bloco
`78xx` sugere que a contaminação vem em lote, por faixa de handle — vale
varrer essa faixa inteira antes de dar o acervo por fechado.
**Correção 12/09 (noite) — a regra de validação estava errada.** O `subidor.py`
só aceitava JPEG/PNG e **apagava** o resto como "bitstream de licença". Pelo
próprio `subidor.log`, dos **40** arquivos que ele apagou assim, **só 1 era
texto de licença**: 36 eram **TIFF** (`II*\0`, ~8 MB, com extensão `.jpg`),
2 **BMP** e 1 **ZIP** (`001AAN014011.jpg.zip`, item `2384`). Resultado, item a
item (lista com URL em `pipeline/apagados-pelo-subidor-antigo.jsonl`):
- **8 itens ficaram sem nenhuma imagem de preservação**: `14358`, `14359`,
`14360`, `14361`, `14362`, `14363`, `14364`, `14368` (TIFF, série "Alb 0261").
- **18 itens** perderam só bitstreams extras (`extra_seqN`, TIFF), mas têm a
imagem principal no bucket ou na fila. `10993`/`10995` (BMP) e `2384` (ZIP)
voltaram por rebaixa e estão na fila.
O `subidor-paralelo.py` aceita TIFF/BMP/GIF/JP2/WebP pela assinatura e manda
qualquer outra coisa para `pipeline/rejeitados/` com linha na tabela
`rejeitados` — nunca apaga. Os 8 casos `78xx` de thumbnail com texto de licença
(acima) foram detectados pelo `vipsthumbnail`, não por essa regra, e continuam
valendo.
### Achado 2.20 — 3.511 duplicatas `(N)` no bucket, do Chrome rebaixando por cima
Objetos `arquivo (1).jpg` convivendo com `arquivo.jpg` byte a byte idêntico —
sufixo que o Chrome põe ao rebaixar um arquivo que já existe em `~/Downloads`,
e que o `subidor.py` subiu como se fosse nome legítimo. Confirmadas idênticas
por **ETag + tamanho** (não só tamanho) antes de apagar: 3.511 objetos,
0,39 GB, removidos do bucket e do ledger na mesma passada.
**Não** foram tocados, e precisam de decisão:
- **30 objetos** cujo gêmeo tem tamanho **diferente** — não são duplicata,
são versões distintas do mesmo nome. Precisam de conferência individual.
- **43 objetos** cujo nome tem `(N)` mas que são o **único** arquivo do item —
candidatos a renomear, nunca a apagar.
`aws s3api list-objects-v2` com `--query "Contents[].[Key,Size,ETag]"` resolve
isso sem baixar nada; o ETag é o MD5 pra upload simples (só os 2.612 objetos
multipart do bucket não servem pra comparação direta).
### Achado 3.3 — o `thumbnail/` do DSpace não é uma miniatura
Medido em amostra de 300: **721 a 3.709 px no maior lado, média 1.087**; menor
lado nunca abaixo de 462. São derivados de resolução média, não miniaturas de
listagem. Duas consequências:
1. Servem tão bem quanto a imagem de preservação pro tile de 128px do mural —
por isso deu pra cobrir os 164 itens sem preservação sem perda visível.
2. A linha "Miniatura | ~13.005 elegíveis" do MANIFEST descreve ~1 MB por
arquivo, não ~8 KB. Quem ler esperando miniatura vai baixar 100x mais do
que imagina.
### Achado 3.4 — publicar o mural agora exige mandar o `atlas/` junto
O `wall.html` de 12:32 passou a derivar `handle` (chave do map) e `source_url`
(mesmo prefixo pra todo item) em JS, em vez de lê-los de cada peça. O
`summarize()` do `build-atlas.js` acompanhou. Resultado: **8,2 MB de map com
12.983 peças contra 10 MB com 12.552** — menos peso com 431 peças a mais.
Mas isso quebra a nota antiga de que "o `atlas/` já está commitado lá e não
muda a cada alteração do mural": map e `wall.html` agora são um par acoplado
por formato. Publicar um sem o outro deixa o mural sem link e sem busca por
handle.
## Pendente
- [ ] **`dc.rights` ausente em 5.833 itens (44,7%)** — confirmado como característica real da fonte, não bug. Só precisa ficar documentado no MANIFEST final, sem ação de reparo.
- [ ] **`web/build-atlas.js` não está versionado em lugar nenhum** — `brasiliano/` não é repo git e o `xyz` só recebe o `index.html` e o `atlas/`. A correção do `summarize()`/`pickAll()` (achado 3.1) vive só no disco desta máquina; um `rm` leva junto. Mesma exposição vale para `pipeline/` inteiro.
- [ ] **Encolher mais o `atlas-map.json` só com dicionário** (achado 3.2): 8,00 → 5,14 MB crus por ~120 KB no fio. Só vale se o `JSON.parse` no celular virar problema medido — não antes.
- [ ] **30 objetos `(N)` com tamanho diferente do gêmeo** (achado 2.20) — conferência individual, não dá pra automatizar com segurança.
- [ ] **43 objetos `(N)` que são o único arquivo do item** (achado 2.20) — renomear tirando o sufixo, pra o nome não mentir sobre haver um original ao lado.
- [ ] **Handles `6656` e `7939` sem imagem de preservação** (achado 2.19) — rebaixar; o que há hoje na fila local é o bitstream de licença.
- [ ] **Conferir os códigos HTTP que nunca foram tabulados** — o runbook do achado 2.18 manda rodar `FALTA.erros()` no console e rotear por código (404 → reparar URL, 403 → bloqueio de origem, 500 → transitório, retomar). **Esse passo nunca foi executado**: o achado 2.18 concluiu "é recusa da origem" a partir do `status()` agregado, não da tabela de erros por handle. Sem isso não dá pra saber quantos dos 71 são reparáveis.
- [ ] **Reconferir o handle `7523` (HTTP 500)** — registrado uma vez e tratado como falha permanente da origem desde então, mas 500 costuma ser transitório e nunca houve segunda tentativa. **Não dá pra testar por `curl`, e agora está provado com controle** (12/09): com User-Agent, Accept, Accept-Language e Referer de Chrome, o `7523` devolve `403` de 5.484 bytes (página do Cloudflare) — e o item `10`, que sabidamente funciona e está no bucket, devolve **exatamente o mesmo `403` de 5.484 bytes**. É bloqueio geral a requisição sem sessão real, nada específico do `7523`. Precisa da aba; a extensão Claude-in-Chrome seguia desconectada em duas tentativas no mesmo dia.
- [ ] **Reconciliar 13.058 vs 13.060** — o MANIFEST fala em 13.058 itens reais, o `catalogo.jsonl` tem 13.060 handles distintos. Diferença de 2 nunca explicada; anotada no MANIFEST como pendência.
- [ ] **Depois que os 212 faltantes baixarem e subirem** (pedido do usuário, 13/09):
1. conferir se **todos** os itens têm miniatura em `derivados/thumb256/` e gerar
as que faltarem (as novas preservações não têm derivado ainda);
2. **regerar o mural 3D** com o conjunto completo — de preferência derivando
no finland direto do bucket (`itens/*/preservacao/*`), que é tráfego
intra-Hetzner, em vez de puxar pro Mac;
3. republicar `wall.html` + `atlas/` no `rafapolo/xyz` (par acoplado, achado 3.4).
- [ ] **Quando o `rodar-paralelo.sh` terminar** (log termina em `===== ... FIM`):
1. conferir no log as três fases sem `!!!`, a linha `FIM da fila` (enviados,
duplicatas, rejeitados, falhas) e os `pendente:` que ficaram no disco;
2. `ls pipeline/rejeitados/` + `select * from rejeitados` — só deve ter texto
de licença; imagem ali é bug de assinatura;
3. reconciliar ledger × bucket (mesmo diff do achado de 12/09: 0 divergências
nos dois sentidos) e amostrar manifests (`grep derivado` = 0);
4. varrer `~/Downloads`: apagar o que tiver MD5 = ETag no bucket (mesmo
método dos 180 de 12/09), manter o resto;
5. atualizar a tabela de contagens do MANIFEST com os números fechados.
- [ ] **Rebaixar o que o `subidor.py` antigo apagou** (achado 2.19, correção): 8 itens sem preservação (`14358`–`14364`, `14368`) + ~20 `extra_seqN` em TIFF — lista com URL em `pipeline/apagados-pelo-subidor-antigo.jsonl`. Precisa da aba do navegador.
- [x] ~~Recontar os faltantes de preservação (211)~~ — feito em 13/09: 136 baixaram, sobraram **75** com defeito de origem (achado 2.25), listados em `pipeline/falhas-500-2026-09-13.txt`. Script usado: `pipeline/baixar-faltantes-v3.js` (ou o motor `F5` colado direto na aba).
- [ ] **`subidor.py` antigo: aposentar ou corrigir a regra de assinatura** antes de qualquer uso futuro — do jeito que está, apaga TIFF.
- [ ] **Rebaixar os 75 do achado 2.25** quando a origem se recuperar — todos têm thumbnail, falta a preservação. Um `FALTA3.retomar()`/`F5.iniciar()` na aba basta; o que já baixou fica marcado no `localStorage` (`bras_faltantes_v3`).
- [ ] **Handle `8396` sem imagem e sem diagnóstico** — não está em `pipeline/faltantes-preservacao.jsonl` nem nas exceções conhecidas do MANIFEST; apareceu ao cruzar catálogo × miniaturas. Precisa de uma tentativa avulsa pra saber se é recusa da origem (achado 2.18) ou outra coisa.
- [ ] **`.git` do `rafapolo/xyz` está em 2,5 GB** e cada publicação do atlas soma ~51 MB permanentes (binário não faz delta). Se o mural for republicar atlas com frequência, tirar os `.webp` do git — LFS, ou servir do próprio bucket, que já tem os thumbs em `derivados/thumb256/`.
- [ ] **Crawl da Fase 1 pode ter mais buracos** (achado 2.10 do PLANO.md): 2 itens reais (`1799`, `4798`) foram achados faltando no catálogo só porque o WordPress os citava — já corrigidos, mas não existe verificação exaustiva de que a enumeração por `browse?type=title` pegou 100% de cada comunidade. Sem ideia barata de auditar isso por completo agora.
## Ao final (não fazer antes do download terminar)
- [x] Gerar `MANIFEST.md` (o que é, como foi coletado, quando) — feito; atualizado em 2026-09-12 com `derivados/` e os achados 2.19/2.20/3.3
- [ ] Confirmar no bucket que a contagem de itens/checksums bate com os 13.058 itens reais esperados
## Notas técnicas úteis pra retomar
- Downloads automáticos do Chrome exigem permissão manual em `chrome://settings/content/automaticDownloads` (site adicionado: `https://brasilianafotografica.bn.gov.br`) — já concedida, vale pra novos scripts também, não só o original.
- `BRAS`, `BAIXA`, `BAIXA_THUMB`, `BAIXA_EXTRA` — todos no `window` da mesma aba, mesmo IndexedDB (`bras`). Todos sobrevivem a fechar/reabrir a aba (dado fica salvo), mas o *loop* para e precisa recolar o script certo pra retomar.
- **Regerar as miniaturas derivadas** (tudo reconstruível, nada precisa ser
guardado): no finland, `apt install libvips-tools parallel` + s5cmd; pra cada
chave de `itens/*/preservacao/*`, `s5cmd cat` num tmp e
`vipsthumbnail "$TMP" --size 256 --output "$OUT[Q=80,strip]"`, com
`parallel -j 10`. ~640 thumbs/min, tráfego intra-Hetzner (bucket é `hel1`, o
finland também) — não passa pelo link de casa. O que ainda está só em
`~/Downloads` sai mais barato derivado localmente com `magick`: mandar os
71 GB pro finland custaria os mesmos dias do upload pendente.
- **Cuidado ao parsear `s5cmd ls`**: nomes de arquivo do acervo têm espaço
**duplo** (`48094 Álbum 311 - ...jpg`). `awk '{$1="";$2="";$3=""; print}'
recolapsa esses espaços e produz chaves que não existem — 31 downloads
falharam assim antes de eu perceber. Use
`sed -E 's|^[0-9/]+ +[0-9:]+ +[0-9]+ ||'`, que preserva o resto literal.
- **`sqlite3` desta máquina tem `.sqliterc` forçando modo box**: qualquer
script que leia a saída precisa de `sqlite3 -init /dev/null -noheader
-separator $'\t'`, senão vem borda de tabela no lugar dos dados (me custou
um diff inteiro de falso positivo).
- **`until ! pgrep -f X; do sleep; done` nunca termina**: a linha de comando do
próprio loop contém `X`, então o `pgrep -f` se acha e a condição nunca vira
falsa. Um loop desses ficou girando horas depois do trabalho acabar (12/09,
morto à mão). Esperar por PID (`while kill -0 $PID`) ou usar `pgrep -f
'[t]humb-local'` para o padrão não casar consigo mesmo.
- **Parar processo lançado em background: SIGTERM, não SIGINT.** Processo
iniciado com `&` num shell não interativo herda o SIGINT ignorado (o
`subidor.py` ficou 60 s vivo depois do `kill -INT`). Com SIGTERM, o `aws s3
cp` filho termina órfão, o que é inofensivo: sem linha no ledger e sem
apagar o local, o próximo uploader sobe de novo a mesma chave.
- **Upload de casa: 69 Mbps** (`networkQuality -s`, 12/09); download 312 Mbps.
- **Assinaturas aceitas como imagem** (`subidor-paralelo.py`): JPEG `FFD8`, PNG,
TIFF `II*\0`/`MM\0*`, GIF, JP2, BMP `BM`, WebP. Content-Type sai do conteúdo,
não da extensão (TIFF chega com nome `.jpg`).
- Scripts-fonte em `pipeline/`: `coletor.js` / `coletor-resume.js` (metadados), `baixador-navegador.js` (preservação, todos os itens), `baixador-thumb.js` (miniaturas), `baixador-extras.js` (2a+ imagem de itens multi-bitstream), `reparo-multibitstream.js` (enumera bitstreams reais de um item).
+42
View File
@@ -0,0 +1,42 @@
> **Arquivado em 2026-09-10 — superado.** Prompt de handoff original da Fase
> 1. Cumpriu seu papel (a Fase 1 terminou, catálogo saiu). Boa parte do
> conteúdo abaixo (trilha A via servidor, "não mexa na aba do Brave") não
> vale mais — ver `tasks/PLANO.md` pro estado e runbook atuais. Mantido só
> como registro histórico.
Retomando o projeto Brasiliana → S3. Leia primeiro `tasks/PLANO.md` (estado, achados, runbook) e depois `README.md` se precisar de mais detalhe técnico.
> Nota (2026-09-10): reorganização de pastas — `baixador.py`, `subidor.py`, `coletor.js`, `baixador-navegador.js`, `catalogo.jsonl` e `.s3env` agora moram em `pipeline/`. Os comandos deste arquivo ainda usam os nomes antigos sem prefixo; rode-os de dentro de `pipeline/` (`cd pipeline` primeiro) ou ajuste os caminhos. `tasks/PLANO.md` já está com os caminhos corrigidos — prefira aquele runbook.
Contexto rápido: a Fase 1 (coletor.js, metadados) está rodando numa aba do Chrome controlada por outra sessão de Claude (Cowork, na nuvem) — não mexa nela, ela é independente. Ela vai exportar `brasiliana-catalogo.jsonl` para `~/Downloads` sozinha quando terminar (ETA ~15-16h a partir de ~10:31 de 2026-09-09, checar se já não terminou antes de você ler isso).
Seu trabalho é tocar as Fases 2-4 assim que o catálogo existir — você tem rede real (SSH, S3), a sessão na nuvem não tem.
1. Verifique se `~/Downloads/brasiliana-catalogo.jsonl` (ou algo com esse padrão de nome) já existe. Se não, esse é o bloqueador: espere ou me avise para eu checar o progresso da fase 1.
2. Fase 2 (trilha A, servidor Finlândia):
```bash
scp baixador.py catalogo.jsonl finland:/root/brasiliana/
ssh finland
set -a; . /root/.s3env; set +a
cd /root/brasiliana
python3 baixador.py --catalogo catalogo.jsonl --pular-legado
python3 baixador.py --status
```
As credenciais reais (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) para preencher `.s3env` estão em `/Users/polux/Projetos/tocador/.env`. Na Finlândia já deve haver `/root/.s3env` configurado — confirme antes de reescrever.
3. Fase 3 (trilha B, nomes com encoding — precisa do navegador):
- Cole `baixador-navegador.js` no console da MESMA aba do coletor (a aba já tem o catálogo em memória/IndexedDB da fase 1). `BAIXA.quantos()` para saber quantos, `BAIXA.iniciar()` para começar — os arquivos caem em `~/Downloads`.
- Em paralelo, no terminal local:
```bash
set -a; . .s3env; set +a
python3 subidor.py --vigiar --catalogo catalogo.jsonl
```
4. Fase 4 (cópias legadas do objdigital, só alcançável localmente):
```bash
set -a; . .s3env; set +a
python3 baixador.py --catalogo catalogo.jsonl --somente-legado
```
5. Ao final: gere `MANIFEST.md` (o que é, como foi coletado, quando) e confirme no bucket que a contagem de itens/checksums bate com os 13.036 esperados (ou o número real que saiu da fase 1, que pode ser ligeiramente diferente).
Regras não-negociáveis (do PLANO.md): SHA-256 de cada arquivo, e URL de origem + data/hora UTC de captura em cada `metadata.json`. `dc.rights` e a instituição de origem são preservados por item.
Se algo travar (disco cheio — aborta abaixo de 2GB livres na Finlândia —, erro de rede, item corrompido), pare, registre o que aconteceu em `tasks/PLANO.md` e me avise em vez de insistir.
+15
View File
@@ -0,0 +1,15 @@
# TODO concluído — 2026-09-10
Itens já fechados, tirados do `tasks/TODO.md` ativo pra não poluir. Ver
`tasks/PLANO.md` pros achados permanentes (Cloudflare bloqueando bitstream,
multi-bitstream, padrão de sequência de thumbnail etc.) — isso aqui é só o
histórico de execução.
- [x] **Re-exportar `brasiliana-catalogo.jsonl`** — feito às 10:46, 13.058 itens, 119 com `all_preservation_urls` confirmados no arquivo.
- [x] Reiniciar `subidor.py` com o catálogo atualizado (PID 3171) — confirmado processando.
- [x] **Fase 4 (legado objdigital)**: não era bloqueio Cloudflare — o domínio só responde em HTTPS (HTTP puro tem a porta resetada). Corrigido em `baixador.py` (força https no `legacy_url`). Rodando local (PID 14570, sem navegador, curl direto funciona) — 2.707 itens com `legacy_url`.
- [x] **34 itens que falharam no reparo multi-bitstream**: 33/34 corrigidos com reparo v3 (nome real do arquivo via `<img src>` da miniatura, tira o `.jpg` duplicado, busca por `/bitstream/handle/PREFIX/h/{nome real}` — necessário porque esses itens não tinham `<a href sequence=>` no wrapper e a sequência não era contígua a partir de 1). Extras disparados (`BAIXA_EXTRA`, 63 imagens novas). Catálogo re-exportado (152 itens com `all_preservation_urls` confirmados) e `subidor.py` reiniciado.
- **1 exceção sem conserto**: handle `20.500.12156.1/7523` ("Vista Aérea de Cabo Frio") — `HTTP 500` do próprio servidor da BN ao buscar os bitstreams, não é problema de nome/padrão. Provável arquivo corrompido/quebrado do lado deles. Documentado no MANIFEST final como exceção conhecida.
- **Total: 152/154 itens sem imagem original corrigidos** (152 + o falso-positivo `.../1` + o `7523` = 154).
- [x] **Reorganização de pastas** (feita por outra sessão em paralelo): scripts/estado → `pipeline/`, visualizações → `web/`, `exemplo-item/` → `examples/`, `censo.py` → `archive/`. `subidor.py` morreu no meio da troca (não estava com FD aberto que sobrevivesse ao rename como o `baixador.py` teve) — reiniciado do novo caminho (PID 28923), backlog de ~2.963 arquivos em `~/Downloads` escoou normalmente depois.
- [x] **Atualizar `PLANO.md`** com os achados desta sessão (2.6-2.9): Cloudflare bloqueia bitstream via curl também, estratégia virou "tudo via navegador", multi-bitstream (115 itens), padrão de sequência do thumbnail, objdigital só em HTTPS.