fix(api,web): enrichissement métadonnées — hints locaux et scoring des correspondances

Recherche de fiches par nom peu fiable : sans ISBN, les providers
étaient interrogés avec des titres bruts peu discriminants.

- extraction de hints locaux (titre/auteur/année/isbn du fichier et du
  nom de fichier) persistés dans books.local_metadata_json
- nouveau use-case ScoreMetadataMatch : tri des résultats par score de
  correspondance avant sélection du meilleur candidat
- providers (google-books, open-library, bnf, local) durcis et nourris
  par les hints ; colonne + index local_metadata_json avec migrations
  idempotentes (création et rebuild legacy)
- web : nettoyage de la description de la fiche livre
  (cleanBookDescription, white-space pre-line)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-23 18:07:29 +02:00
parent 5de46a6f6d
commit 18d73a4db6
21 changed files with 829 additions and 56 deletions

View File

@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { cleanBookDescription } from "./description";
describe("cleanBookDescription", () => {
it("renders catalog HTML as readable plain text", () => {
expect(cleanBookDescription("<p>Premier &amp; second.</p><p><strong>Suite</strong>&nbsp;du texte.</p>")).toBe(
"Premier & second.\nSuite du texte."
);
});
it("falls back when the description is empty after cleanup", () => {
expect(cleanBookDescription("<p> </p>")).toBe("Notice absente du catalogue.");
});
});

View File

@ -0,0 +1,28 @@
const blockBreakPattern = /<\/(p|div|section|article|header|footer|blockquote|li|ul|ol|br|h[1-6])>/gi;
const tagPattern = /<[^>]*>/g;
function decodeEntities(value: string): string {
if (typeof document === "undefined") {
return value
.replace(/&nbsp;/gi, " ")
.replace(/&amp;/gi, "&")
.replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">")
.replace(/&quot;/gi, '"')
.replace(/&#39;/gi, "'");
}
const textarea = document.createElement("textarea");
textarea.innerHTML = value;
return textarea.value;
}
export function cleanBookDescription(description?: string | null): string {
if (!description) return "Notice absente du catalogue.";
return decodeEntities(description.replace(blockBreakPattern, "\n").replace(tagPattern, " "))
.replace(/\r/g, "")
.replace(/[ \t]+\n/g, "\n")
.replace(/\n[ \t]+/g, "\n")
.replace(/[ \t]{2,}/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim() || "Notice absente du catalogue.";
}