Archive.org lending clone — modelo y decisiones
Fecha: 2026-08-02
Código: ~/nef/archive-lending-clone
Correr: preview_start lending-clone (puerto 8742) o abrir index.html directo.
Qué es
Clon de la sección de préstamo de libros de archive.org. Estático puro: HTML + CSS + 2 archivos JS, cero dependencias, cero build. Estado en localStorage.
El modelo que se replicó (Controlled Digital Lending)
Esto es lo que hace interesante al original, y lo que vale la pena recordar:
- Un lector por copia. Cada título tiene N copias físicas digitalizadas.
La disponibilidad es
copies - prestadas_a_otros - mi_préstamo - mi_hold. - Préstamo con vencimiento. 14 días, devolución automática al expirar. No hay “renovar” — se vuelve a pedir.
- Waitlist → hold, no waitlist → préstamo. Cuando se libera una copia, el siguiente en fila NO recibe el libro: recibe un hold de 24h. Si no lo canjea, expira y la copia vuelve al pool. Este es el detalle que casi todos los clones omiten y es el que hace que el sistema no se atore.
- Un hold reserva copia física. Cuenta como “fuera” para todos los demás. Si no se modela así, la biblioteca presta copias que no existen.
- El préstamo es el permiso de lectura. Devolver el libro cierra el lector en el acto.
Decisiones de implementación
- Actividad simulada de otros lectores. Sin esto, la waitlist nunca se
resuelve y la feature es decorativa. Cada 30s hay probabilidad de que otro
lector devuelva una copia (
tickLibrary). Es la parte “falsa” del demo y está marcada como tal en el código. - Un solo heartbeat.
expireAll→tickLibrary→promoteWaitlist→render, en ese orden, en boot y cada 30s. El orden importa: expirar antes de promover, o se promueve contra copias que ya se liberaron. - Portadas procedurales. Gradiente HSL por libro vía
--h. Sin imágenes externas, sin assets, sin problemas de derechos. - Lector sin texto. El visor de páginas dibuja reglas tipográficas con CSS en lugar de prosa. El demo no distribuye texto de ningún libro. Nota visible al pie del lector.
- Catálogo de dominio público (
books.js), solo metadatos.
Bugs encontrados al verificar
[hidden]no vencía adisplay:griddel modal → el modal aparecía abierto al cargar. Fix:[hidden]{display:none !important}. Pasa siempre que se combine el atributohiddencon display en CSS.- Paginación arrancaba en página 0 (“Pages 1–1”). El spread debe ser
spread*2+1, nospread*2. - Caché del navegador sirvió JS viejo y produjo una fecha de vencimiento aparentemente errónea. No era bug de código. Verificar con cache-bust antes de perseguir un bug de lógica.
Conexión a PDFs reales (servidor propio)
Los libros propios se declaran a mano en MY_LIBRARY, arriba de books.js:
const PDF_BASE = "https://tuservidor.com/libros";
const MY_LIBRARY = [
{ id: "rayuela", title: "Rayuela", author: "Julio Cortázar", year: 1963,
subject: "Fiction", copies: 1, pages: 736, coverHue: 15, pdf: "rayuela.pdf" },
];pdfacepta ruta relativa aPDF_BASEo URL completa.resolvePdf()filtra a http/https únicamente — nada más llega a un iframe.copies: 1= un lector a la vez, que es el punto del modelo.- Los libros con PDF llevan cinta “PDF” en la portada y usan el visor real; el catálogo demo conserva las páginas placeholder.
El préstamo controla el acceso. Al devolver o expirar, closeReader()
vacía el innerHTML del stage, lo que destruye el iframe y corta la descarga
en curso. Sin esto el visor seguiría abierto tras devolver.
De dónde salieron los 6 libros cargados
github.com/[usuario]/public_domain_gem no aloja PDFs — es un gem de Ruby que
consulta APIs de dominio público (Archive.org, Europeana, Wikisource, OAI). Los
identificadores de sus fixtures (elingeniosohidalgo, audioquijote) son
ficticios y devuelven metadata vacía.
Lo aprovechable es su patrón de construcción de URL, en archive/item.rb:
https://archive.org/download/<identifier>/<nombre-archivo>
Flujo que se usó para poblar MY_LIBRARY, replicable:
- Buscar:
archive.org/advancedsearch.phpconq=mediatype:texts AND format:"Text PDF" AND language:Spanish AND date:[1700-01-01 TO 1928-12-31],sort[]=downloads desc,output=json. - Por cada identifier, pedir
archive.org/metadata/<id>y quedarse con el archivo de formato PDF más chico (suele ser el_bw.pdf). imagecountde la metadata sirve como conteo de páginas.
Filtrar por date:[… TO 1928-12-31] es lo que mantiene el catálogo en dominio
público. El campo licenseurl viene vacío en muchos items válidos, así que no
sirve como único filtro.
Archive.org sí permite embebido. Verificado: los PDF responden
206 application/pdf y su CSP no trae directiva frame-ancestors ni hay
X-Frame-Options. El iframe dispara load.
Si el iframe sale en blanco es casi siempre el servidor bloqueando embebido. En nginx, para la carpeta de libros:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://tudominio.com" always;El clon ya muestra un aviso a los 3s y un enlace “Open directly ↗”, porque un iframe cross-origin no emite un evento de error confiable.
Rasterizado de páginas
El lector arma cada spread con una de tres fuentes, elegida por libro:
| Modo | Cuándo | Cómo |
|---|---|---|
iiif | el libro trae iiif: "<identifier>" | imágenes de página del servicio IIIF de Archive.org |
pdf | solo pdf:, y el host permite CORS | pdf.js rasteriza a <canvas> |
iframe | pdf falló al cargar | visor nativo embebido, navegación oculta |
placeholder | catálogo demo | reglas tipográficas, sin texto de libro |
El hallazgo que define la arquitectura
Archive.org NO manda Access-Control-Allow-Origin en /download/. Por eso
pdf.js no puede leer esos PDFs desde el navegador — es imposible rasterizarlos
del lado cliente. Pero su servicio IIIF sí manda access-control-allow-origin: *.
Conclusión: para contenido de Archive.org no se rasteriza el PDF, se piden las páginas ya rasterizadas. Es más rápido, no baja 54 MB, y da imágenes reales. pdf.js queda para PDFs propios (servidor con CORS bajo tu control).
URL de página IIIF, sin bajar el manifiesto
https://iiif.archive.org/iiif/<identifier>$<n>/full/,<alto>/0/default.jpg
<n> es 0-indexed (la UI es 1-indexed — de ahí el n - 1 en renderReader).
No hace falta pedir manifest.json por libro salvo para contar páginas.
El conteo de páginas debe salir del manifiesto (items.length), no del
imagecount de la metadata. No coinciden: Quijote da 910 vs 906 reales,
Paraíso perdido 392 vs 388. Usar el imagecount produce 404 en las últimas páginas.
No todo item tiene derivados de imagen
manualpaleografia00riveuoft devuelve 500 en el manifiesto y 404 en toda página
IIIF: existe solo como PDF. Ése es el caso real que justifica el fallback
automático a iframe. No se hardcodea: se intenta pdf.js y si falla, cae solo.
Lo que quedó sin verificar
pdf.js pinta el fondo blanco del canvas y lo dimensiona bien (850×1100 para
RASTER_HEIGHT = 1100), pero los glifos nunca se pintaron en el panel de
previsualización: page.render().promise no resuelve con el viewport en 0×0
porque rAF está estrangulado. El cableado está probado; el resultado visual hay
que confirmarlo en un navegador real.
Si se retoma
Lo que falta para que sea presentable como pieza, no como demo: cola de espera con posición real (“eres el 3 de 7”), historial de préstamos, y estados vacíos con más carácter. El lector es lo que más carga visual aguanta.