feat(api,shared): métadonnées multi-providers et automatisation des scans/enrichissements

Chaîne de résolution metadata (local, Open Library, Google Books, BNF)
avec activation/priorité/clé API par provider, colonnes isbn13 et
identifiers sur les livres, et intégration au scanner pour compléter
métadonnées et jaquettes manquantes. Module automation: réglages
persistés, planifications scan/enrichissement et déclenchement manuel,
exposés via des endpoints admin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-23 13:10:45 +02:00
parent 1ac4144d0e
commit 48e9459cf3
23 changed files with 1029 additions and 24 deletions

View File

@ -0,0 +1,47 @@
import { Injectable } from "@nestjs/common";
import { MetadataLookup, MetadataMatch, MetadataProvider, MetadataProviderConfig } from "../metadata.types.js";
@Injectable()
export class GoogleBooksProvider implements MetadataProvider {
readonly id = "googlebooks" as const;
async lookup(lookup: MetadataLookup, config: MetadataProviderConfig): Promise<MetadataMatch | null> {
const query = lookup.identifiers.isbn13
? `isbn:${lookup.identifiers.isbn13}`
: `intitle:${lookup.title}${lookup.author ? `+inauthor:${lookup.author}` : ""}`;
const url = new URL("https://www.googleapis.com/books/v1/volumes");
url.searchParams.set("q", query);
url.searchParams.set("maxResults", "1");
if (config.apiKey) url.searchParams.set("key", config.apiKey);
const response = await fetch(url, { signal: AbortSignal.timeout(4000) });
if (!response.ok) return null;
const data = (await response.json()) as { items?: Array<{ volumeInfo?: Record<string, unknown> }> };
const info = data.items?.[0]?.volumeInfo;
if (!info) return null;
return {
title: stringValue(info.title) ?? undefined,
author: arrayJoin(info.authors),
description: stringValue(info.description),
language: stringValue(info.language),
publisher: stringValue(info.publisher),
publishedDate: stringValue(info.publishedDate),
isbn: isbnFromIndustryIdentifiers(info.industryIdentifiers)
};
}
}
function stringValue(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function arrayJoin(value: unknown): string | null {
return Array.isArray(value) && value.length ? value.map(String).join(", ") : null;
}
function isbnFromIndustryIdentifiers(value: unknown): string | null {
if (!Array.isArray(value)) return null;
const isbn13 = value.find((entry) => entry?.type === "ISBN_13")?.identifier;
const isbn10 = value.find((entry) => entry?.type === "ISBN_10")?.identifier;
return stringValue(isbn13) ?? stringValue(isbn10);
}