ReadaBook
ReadaBook est une application locale-first pour cataloguer, rechercher et lire une bibliothèque personnelle de livres EPUB/PDF stockés sur disque. Le MVP livré vise un usage domestique : un administrateur déclare un dossier local, lance un scan, puis les livres deviennent accessibles via un catalogue web et une API locale.
Fonctionnalités MVP présentes
- Backend NestJS/Fastify exécutable avec healthcheck
GET /healthz. - Base SQLite locale avec Drizzle, WAL, migrations idempotentes au démarrage et index FTS5.
- Auth locale : bootstrap du premier admin, login/logout, cookie JWT httpOnly, rôles
admin/user. - Administration : CRUD utilisateurs, CRUD bibliothèques, déclenchement de scan, consultation des jobs.
- Scan récursif de bibliothèques EPUB/PDF.
- Extraction locale pragmatique : métadonnées EPUB, métadonnées PDF simples, jaquettes EPUB quand présentes.
- Enrichissement opportuniste Open Library, désactivable.
- Catalogue : liste, recherche FTS, fiche livre, stream fichier, stream couverture.
- Progression utilisateur : sauvegarde d’un locator/percent et étagère “continuer”.
- Frontend PWA Vite/React dans
apps/web, installable en mode standalone et servi par Nginx en compose. - Expérience web “cabinet de curiosités numérique” : login, bootstrap admin, accueil, bibliothèques, recherche, fiches livre, lecteur, profil et administration.
- Lecteur web : rendu PDF via
pdfjs-dist, adaptateur EPUBfoliate-js, reprise de progression via/progress.
Stack actuelle
- Monorepo
pnpm. apps/api: NestJS 11, Fastify, SQLitebetter-sqlite3, Drizzle ORM, FTS5, Argon2, JOSE JWT.apps/web: Vite, React, TypeScript,lucide-react,pdfjs-dist,foliate-js, service worker et manifeste PWA, Nginx runtime.packages/shared: contrats Zod partagés.- Docker multi-stage Node 22 pour build/runtime API, Nginx pour web.
Structure repo
apps/
api/ Backend NestJS/Fastify
web/ Frontend Vite/React
packages/
shared/ Types et schémas Zod partagés
data/
library/ Dossier local à scanner, monté en lecture seule dans compose
storage/ Cache backend, notamment couvertures extraites
Dockerfile Image API
docker-compose.yaml Services API + web
pnpm-workspace.yaml Workspace monorepo
Développement local
corepack enable
pnpm install
pnpm dev
Scripts utiles :
pnpm dev: lance API + web en parallèle.pnpm dev:api: backend seul.pnpm dev:web: frontend seul.pnpm build: build récursif.pnpm test: tests récursifs.
Note sandbox constatée : sur Node 24 local, better-sqlite3 peut nécessiter un build natif. Le chemin Docker utilise Node 22 et a été validé.
Docker Compose
JWT_SECRET="replace-me" docker compose up --build
Services :
api: API interne surapi:3000, non publiée directement sur l’hôte.web: Nginx + frontend, publié surhttp://localhost:3000.
Ports :
- Hôte
3000-> conteneurweb:80. - L’API est accessible depuis le navigateur via les routes proxifiées par Nginx :
/auth,/admin,/books,/progress,/healthz.
Frontend PWA
Le frontend est accessible sur http://localhost:3000 en compose. Il expose les routes MVP :
/login: connexion locale./setup/*: bootstrap du premier administrateur./home: accueil, reprise de lecture, bibliothèques et catalogue./library/:libraryId: ouvrages d’une bibliothèque./book/:bookId: fiche livre./reader/:bookId: lecture EPUB/PDF avec sauvegarde de progression./search: recherche catalogue./me: profil/session./admin/*: bibliothèques, scans, jobs et comptes.
La PWA fournit manifest.webmanifest, sw.js, une icône maskable SVG et display: standalone. Les appels API passent par le même origin en compose via Nginx, ce qui conserve les cookies httpOnly de session.
Volumes :
./data:/data: base SQLite/data/readabook.sqliteet cache/data/storage.${READABOOK_LIBRARY_HOST_PATH:-./data/library}:/library:ro: bibliothèque locale scannée en lecture seule.
Variables principales :
JWT_SECRET: secret JWT, à changer hors développement.OPEN_LIBRARY_ENABLED=true|false: active/désactive l’enrichissement distant.READABOOK_LIBRARY_HOST_PATH: dossier hôte monté en lecture seule sur/library.READABOOK_LIBRARY_ALIAS_FROM: chemin alternatif accepté par l’API et traduit vers/library.DATABASE_PATH=/data/readabook.sqliteSTORAGE_DIR=/data/storage
Bootstrap admin
Après démarrage compose :
curl -c cookies.txt \
-H "content-type: application/json" \
-d '{"email":"admin@example.com","password":"password123","name":"Admin"}' \
http://localhost:3000/auth/bootstrap
Connexion :
curl -c cookies.txt -b cookies.txt \
-H "content-type: application/json" \
-d '{"email":"admin@example.com","password":"password123"}' \
http://localhost:3000/auth/login
Ajouter une bibliothèque /library
Place les fichiers EPUB/PDF dans data/library côté hôte, puis déclare le volume monté dans le conteneur :
curl -b cookies.txt \
-H "content-type: application/json" \
-d '{"name":"Bibliothèque locale","path":"/library","enabled":true}' \
http://localhost:3000/admin/libraries
Pour tester le corpus réel local Books/ sans le versionner, crée un .env local :
READABOOK_LIBRARY_HOST_PATH=./Books
READABOOK_LIBRARY_ALIAS_FROM=/chemin/absolu/vers/ReadaBook/Books
QA peut ensuite créer la bibliothèque avec le chemin absolu saisi dans READABOOK_LIBRARY_ALIAS_FROM;
l’API le traduit vers /library, puis le scan manuel teste l’extraction ISBN/métadonnées/jaquettes sur ce corpus.
Lancer un scan
curl -b cookies.txt -X POST http://localhost:3000/admin/libraries/1/scan
curl -b cookies.txt http://localhost:3000/admin/jobs
curl -b cookies.txt http://localhost:3000/books
Endpoints principaux
GET /healthzPOST /auth/bootstrapPOST /auth/loginPOST /auth/logoutGET /auth/meGET|POST /admin/usersGET|POST|PATCH|DELETE /admin/librariesPOST /admin/libraries/:id/scanGET /admin/jobsGET /booksGET /books/search?q=termGET /books/:idGET /books/:id/fileGET /books/:id/coverGET|PUT /progress/:bookIdGET /progress/continue
Limites connues
- Extraction PDF limitée aux champs metadata simples, sans OCR ni parsing complet.
- Extraction EPUB correcte pour les cas standards, pas exhaustive sur tous les EPUB malformés.
- Open Library est opportuniste : échec silencieux en cas d’erreur réseau ou de rate limit.
- Jobs in-process : pas de worker externe, pas de reprise fine d’un scan interrompu.
- Pas encore de migrations versionnées Drizzle Kit ; migrations SQL idempotentes exécutées au démarrage.
- L’API n’est pas exposée directement en compose ; elle passe par le reverse proxy Nginx du service web.