From d56061db6b8aef9d6f033af3392913dd17f0974e Mon Sep 17 00:00:00 2001 From: mathieu Date: Mon, 17 Aug 2026 21:01:46 +0200 Subject: [PATCH] Sortir les sources du tarball et acter ZXing.Net pour le scan ISBN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le dépôt ne versionnait qu'une archive `mabibli-init.tar.gz`, ce qui empêchait git de suivre le contenu des fichiers. Les trois fichiers sont désormais à plat et l'archive est supprimée. Scan ISBN : remplacement de `html5-qrcode` par ZXing.Net (C#, Apache 2.0). Validé concrètement en .NET 10 — round-trip EAN-13 OK, publish Blazor WASM OK, 0,53 ms/frame dans le pire cas (échec sur frame 640x480 bruitée), +192 Ko brotli sur le payload. Motif principal : le décodage reste réutilisable hors navigateur si le projet évolue en scanner de bibliothèque. Clarification du besoin hors-ligne : consultation de la bibliothèque existante uniquement. Signale au passage que SQLite vit côté serveur et qu'un cache client sera nécessaire — point d'architecture non résolu. Co-Authored-By: Claude Opus 5 --- .gitignore | 19 +++++ CLAUDE.md | 169 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 29 ++++++++ mabibli-init.tar.gz | Bin 3507 -> 0 bytes 4 files changed, 217 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md delete mode 100644 mabibli-init.tar.gz diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3b96d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# .NET +bin/ +obj/ +publish/ +*.user +*.suo + +# SQLite +*.db +*.db-shm +*.db-wal + +# IDE +.vs/ +.vscode/ +*.swp + +# OS +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..64582df --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,169 @@ +# CLAUDE.md — Contexte projet MaBibli + +Ce fichier donne le contexte complet du projet à Claude Code. Lis-le en entier avant de commencer à coder. + +## Objectif du projet + +Application self-hosted de gestion de bibliothèque personnelle, à héberger sur YunoHost, accessible depuis smartphone et PC. + +## Fonctionnalités attendues (v1) + +1. **Catalogue de livres physiques** — liste, ajout, édition, suppression +2. **Catalogue de livres numériques (ebooks)** — même chose, avec un champ format distinct du physique +3. **Gestion de prêts** + - Prêter un livre à une personne (nom, date de prêt) + - Marquer comme récupéré (date de retour) + - Historique des prêts passés par livre (pas juste l'état courant) +4. **Récupération automatique des infos via ISBN** + - Scan caméra du code-barres (EAN-13 / ISBN) + - Saisie manuelle de l'ISBN + - Dans les deux cas : appel à une ou plusieurs API pour pré-remplir titre, auteur, éditeur, couverture +5. **Statuts de lecture** — à lire / en cours / lu (au minimum), assignable à chaque livre + +## Décisions techniques actées + +| Sujet | Décision | Pourquoi | +|---|---|---| +| Langage backend | C# / ASP.NET Core | Choix de l'utilisateur, typage fort | +| Frontend | Blazor WebAssembly | Support PWA quasi natif (`dotnet new blazorwasm --pwa`), tout en C#, offline partiel | +| Base de données | SQLite + Entity Framework Core | Fichier unique, pas de serveur DB séparé, adapté à un usage perso/familial | +| Auth / multi-utilisateur | Déléguée au SSO de YunoHost | Pas de système de login custom à maintenir | +| Hébergement | YunoHost, installation **native** (pas Docker) | YunoHost déconseille Docker pour ses apps (moins fiable, plus lourd) ; installation native = meilleures perfs sur petit matériel | +| Packaging YunoHost | S'inspirer de [`radarr_ynh`](https://github.com/YunoHost-Apps/radarr_ynh) | Radarr est aussi en .NET, packagé sans Docker sur YunoHost. Leur `manifest.toml` montre un déploiement **self-contained** (`dotnet publish -r linux-x64 --self-contained`), donc pas besoin d'installer `dotnet-runtime` via apt côté serveur — le binaire embarque son propre runtime | +| Scan ISBN | **ZXing.Net** (C#, Apache 2.0) exécuté dans le WASM ; le JS ne fournit que les pixels caméra | Décodage en C#, réutilisable hors navigateur si le projet évolue en scanner de bibliothèque. Voir la section dédiée ci-dessous | +| Consultation hors-ligne | Cache local des données côté client (mécanisme à trancher) | Le besoin est de **consulter la bibliothèque existante** sans réseau, pas d'enrichir de nouveaux livres. Voir « Stratégie hors-ligne » | + +## Scan du code-barres — ZXing.Net (décision actée) + +Le décodage EAN-13 se fait en **C# avec [ZXing.Net](https://www.nuget.org/packages/ZXing.Net)** (`micjahn`, Apache 2.0), et non avec une bibliothèque JS type `html5-qrcode`. + +### Pourquoi + +- **Réutilisable hors navigateur.** Si le projet évolue vers un scanner de bibliothèque (app native, scan en masse, décodage d'une photo côté serveur), le code de décodage se transpose tel quel. Une bibliothèque JS serait à réécrire intégralement. +- **Un seul langage**, cohérent avec le reste de la stack. +- L'argument « offline » n'entre **pas** en compte ici : `html5-qrcode` fonctionne aussi hors-ligne (fichier JS servi par la PWA, aucun appel réseau). Ce n'est pas un critère de départage. + +### Mesures réelles (validées sur .NET 10, publish Blazor WASM OK) + +| Mesure | Résultat | +|---|---| +| Décodage EAN-13 propre (380×160) | 0,04 ms/frame | +| **Pire cas** : frame 640×480 bruitée sans code-barres (échec) | 0,53 ms/frame | +| Surcoût du payload PWA | +192 Ko (brotli) | + +Le pire cas est le chiffre qui gouverne le framerate : la majorité des frames caméra ne contiennent pas de code-barres lisible, et c'est l'échec de décodage qui coûte le plus cher. + +⚠️ Ces chiffres sont mesurés en **JIT x64 natif**. En Blazor WASM le code est *interprété* par défaut : compter un facteur ~10-20×, soit ~5-10 ms/frame — largement suffisant pour scanner à 10-15 fps. Activer `true` ramène ça à 1-2 ms, au prix d'un build nettement plus lent. + +### Pièges à connaître + +- **Le JS interop ne disparaît pas.** `getUserMedia` et ``/`getImageData` sont des API web sans équivalent C#. Prévoir ~30 lignes de JS maison dont le seul rôle est de pousser un `byte[]` vers C#. Toute la logique de décodage reste en C#. +- **Ne pas perdre de temps à essayer de réduire la taille via un reader ciblé.** Remplacer `MultiFormatReader` par `EAN13Reader` pour aider le trimmer **ne change rien** : mesuré à 192 495 octets à l'octet près dans les deux cas. ZXing.Net n'est pas trim-friendly. +- `RGBLuminanceSource` accepte directement le buffer RGBA du canvas (`BitmapFormat.RGBA32`) — **aucune bibliothèque d'image nécessaire** (pas de SkiaSharp ni ImageSharp). +- Le scan caméra exige **HTTPS** (garanti par YunoHost en prod ; en dev, `localhost` est considéré comme sûr). + +### Squelette validé + +```csharp +using ZXing; +using ZXing.Common; + +public static class IsbnScanner +{ + static readonly MultiFormatReader Reader = new() + { + Hints = new Dictionary + { + [DecodeHintType.POSSIBLE_FORMATS] = new List + { + BarcodeFormat.EAN_13, BarcodeFormat.EAN_8, + }, + [DecodeHintType.TRY_HARDER] = true, + }, + }; + + /// rgba : buffer brut issu de ctx.getImageData(...).data + public static string? TryDecode(byte[] rgba, int width, int height) + { + var source = new RGBLuminanceSource( + rgba, width, height, RGBLuminanceSource.BitmapFormat.RGBA32); + return Reader.decode(new BinaryBitmap(new HybridBinarizer(source)))?.Text; + } +} +``` + +## Stratégie hors-ligne — à trancher avant de coder + +**Besoin réel** : consulter la bibliothèque **déjà enregistrée** sans réseau (liste des livres, statuts, prêts en cours). Il ne s'agit **pas** d'enrichir de nouveaux livres hors-ligne — le lookup ISBN exige de toute façon un accès à OpenLibrary. + +⚠️ **Point d'architecture non résolu** : « les données sont déjà en local » n'est vrai qu'au sens *serveur*. En Blazor WebAssembly, le code tourne dans le navigateur, alors que SQLite vit côté serveur. Le service worker de la PWA met en cache les *assets* (HTML/CSS/WASM), mais **pas les réponses de l'API**. Sans travail supplémentaire, l'app se lancera hors-ligne mais affichera une liste vide. + +Options à évaluer : + +1. **Cache client des réponses API** (IndexedDB, ou cache du service worker sur les routes `GET`) — le plus simple, lecture seule, suffisant pour le besoin exprimé. +2. **SQLite compilé en WASM côté client**, avec persistance IndexedDB/OPFS, synchronisé avec le serveur — bien plus lourd, ne se justifie que si l'écriture hors-ligne devient nécessaire. + +Par défaut, partir sur l'option 1 tant que le besoin reste la consultation. + +## Sources de données ISBN — point d'attention important + +**Ne pas dépendre d'une seule source, et éviter Google Books si possible** (préférence explicite de l'utilisateur : pas de dépendance à Google). + +Stratégie : interroger plusieurs sources **libres et gratuites, sans clé API obligatoire**, en cascade (si la première ne répond pas ou renvoie des données incomplètes, essayer la suivante) : + +1. **OpenLibrary** (`https://openlibrary.org/isbn/{isbn}.json`) — source principale, gratuite, sans clé + - ⚠️ **Bug connu identifié lors des tests avec BookLogr** : l'endpoint `/isbn/{isbn}.json` renvoie souvent le titre mais l'**auteur est juste une référence** (`/authors/OL...A`), pas le nom directement. Il faut faire un **second appel** vers `/authors/{id}.json` pour récupérer le nom. Un projet qui oublie ce second appel se retrouve avec titre rempli mais auteur vide (symptôme exact observé et diagnostiqué chez BookLogr) — **ne pas reproduire ce bug**. + - La couverture est un service séparé : `https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg` (peut exister même si la fiche bibliographique est incomplète) +2. **Fallback envisageable** : autres sources ouvertes (à évaluer — éviter Google Books sauf si l'utilisateur change d'avis explicitement) + +## Modèle de données (base de départ, à affiner) + +``` +Livre +├── Id +├── Isbn +├── Titre +├── Auteur +├── Editeur +├── Format : Physique | Numerique +├── Statut : ALire | EnCours | Lu +├── CoverUrl +├── DateAjout +└── UtilisateurId (propriétaire, via SSO YunoHost) + +Pret +├── Id +├── LivreId (FK vers Livre) +├── Emprunteur (nom, texte libre) +├── DatePret +└── DateRetour (nullable — NULL tant que non rendu) +``` + +Garder `Pret` comme table séparée (pas un champ sur `Livre`) pour conserver l'historique complet des prêts passés, pas juste l'état actuel. + +## Historique du projet (pourquoi ces choix) + +L'utilisateur a testé deux solutions existantes avant de se lancer dans un projet custom : + +- **uBiblio** (Docker, Python) — gère prêts + scan ISBN, mais dépend de Google Books (clé API requise) pour l'autofill, ce qui ne convient pas à l'utilisateur qui veut éviter cette dépendance +- **BookLogr** (Docker, Python) — utilise OpenLibrary nativement mais a le bug décrit ci-dessus (auteur non récupéré) + +Conclusion : aucune des deux solutions existantes ne coche toutes les cases (prêts + pas de dépendance Google + autofill fiable) → développement d'une solution sur mesure. + +## Prochaines étapes suggérées + +1. Scaffolder le projet Blazor WebAssembly PWA (`dotnet new blazorwasm --pwa`) — **commande vérifiée valide en .NET 10**, l'option `--pwa` existe toujours +2. Ajouter le **projet API ASP.NET Core** : le client WASM tourne dans le navigateur, SQLite vit côté serveur — une API est indispensable, elle n'était pas explicitée dans la version initiale de ce document +3. Mettre en place le modèle EF Core + SQLite + migrations +4. Implémenter le service de lookup ISBN (OpenLibrary avec le double-appel titre+auteur) +5. CRUD livres (physique/numérique, statuts de lecture) +6. Gestion des prêts +7. Intégration scan caméra (**ZXing.Net** + interop caméra minimal) +8. Cache hors-ligne pour la consultation (voir « Stratégie hors-ligne ») +9. Packaging YunoHost (`manifest.toml`, `conf/systemd.service`, `conf/nginx.conf`, `scripts/install`) en s'inspirant de radarr_ynh + +## Questions ouvertes + +- **Mécanisme exact du SSO YunoHost** : le principe est acté (pas d'auth custom), mais l'implémentation reste à préciser — vraisemblablement la lecture d'en-têtes injectés par SSOwat derrière nginx (`Remote-User`). Cela conditionne le remplissage de `UtilisateurId`. +- **Sources ISBN de fallback** : toujours « à évaluer ». Candidats sans clé API et hors Google : OpenLibrary Search API, BnF (SRU — pertinent pour le fonds francophone), Wikidata. +- **Choix du cache hors-ligne** : voir la section dédiée. diff --git a/README.md b/README.md new file mode 100644 index 0000000..29b9313 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# MaBibli + +Application de gestion de bibliothèque personnelle, self-hosted sur YunoHost. + +## Demande initiale + +Gérer une bibliothèque personnelle (livres physiques et numériques), avec : + +- **Liste des livres physiques** +- **Liste des livres ebooks** +- **Gestion de prêts** — prêter un livre à quelqu'un, marquer comme récupéré, historique des prêts +- **Récupération automatique des infos via ISBN** (titre, auteur, éditeur, couverture) — scan caméra + saisie manuelle +- **Consultation hors-ligne** de la bibliothèque existante (le lookup ISBN, lui, nécessite le réseau) +- **Statuts de lecture** — à lire, en cours, lu, etc. + +## Contraintes techniques + +- **Langage** : C# / ASP.NET Core +- **Frontend** : Blazor WebAssembly, en **PWA** (installable, utilisable hors-ligne pour la consultation de la bibliothèque déjà enregistrée) +- **Scan code-barres** : **ZXing.Net** (décodage EAN-13 en C#, pas de bibliothèque JS tierce) +- **Accès** : smartphone (GSM) et PC, via navigateur +- **Multi-utilisateur** : géré via le SSO de **YunoHost** (pas de système d'auth custom) +- **Base de données** : **SQLite** +- **Hébergement** : **YunoHost**, en installation **native (sans Docker)** — packaging façon `_ynh`, inspiré de [radarr_ynh](https://github.com/YunoHost-Apps/radarr_ynh) (déploiement .NET self-contained, pas de dépendance dotnet-runtime côté système) +- **Sources ISBN** : interrogation de **plusieurs bases de données libres/sans restriction** en cascade (pas de dépendance à une seule source propriétaire type Google Books) — voir `CLAUDE.md` pour le détail + +## Statut + +Projet en tout début de structuration. Voir `CLAUDE.md` pour le contexte complet à destination de Claude Code. diff --git a/mabibli-init.tar.gz b/mabibli-init.tar.gz deleted file mode 100644 index 0b9cc5ace53d410897446e55209dee9b8bcebff6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3507 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