fix(api): robustesse des providers de métadonnées
Recherche ISBN directe sur Open Library avec repli titre/auteur, normalisation des réponses, timeouts et User-Agent explicites sur les trois providers distants, abandon de l'extraction de jaquette CBR en échec silencieux (fallback métadonnées seules), tests des providers et garde du test de migration si better-sqlite3 est indisponible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,15 +1,21 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { MetadataLookup, MetadataMatch, MetadataProvider, MetadataProviderConfig } from "../metadata.types.js";
|
||||
import { toIsbn13 } from "../use-cases/extract-identifiers.js";
|
||||
|
||||
@Injectable()
|
||||
export class OpenLibraryProvider implements MetadataProvider {
|
||||
readonly id = "openlibrary" as const;
|
||||
|
||||
async lookup(lookup: MetadataLookup, _config: MetadataProviderConfig): Promise<MetadataMatch | null> {
|
||||
const query = lookup.identifiers.isbn13
|
||||
? `isbn:${encodeURIComponent(lookup.identifiers.isbn13)}`
|
||||
: `title:${encodeURIComponent(lookup.title)}${lookup.author ? ` author:${encodeURIComponent(lookup.author)}` : ""}`;
|
||||
const response = await fetch(`https://openlibrary.org/search.json?q=${query}&limit=1`, {
|
||||
const isbn = lookup.identifiers.isbn13 ?? lookup.identifiers.isbn10;
|
||||
if (isbn) {
|
||||
return this.lookupIsbn(isbn, lookup.identifiers.isbn13);
|
||||
}
|
||||
const query = `title:${lookup.title}${lookup.author ? ` author:${lookup.author}` : ""}`;
|
||||
const url = new URL("https://openlibrary.org/search.json");
|
||||
url.searchParams.set("q", query);
|
||||
url.searchParams.set("limit", "1");
|
||||
const response = await fetch(url, {
|
||||
headers: { "User-Agent": "ReadaBook/0.1 local-library-manager" },
|
||||
signal: AbortSignal.timeout(4000)
|
||||
});
|
||||
@ -18,16 +24,77 @@ export class OpenLibraryProvider implements MetadataProvider {
|
||||
const doc = data.docs?.[0];
|
||||
if (!doc) return null;
|
||||
return {
|
||||
author: firstArrayValue(doc.author_name),
|
||||
title: stringValue(doc.title) ?? undefined,
|
||||
author: arrayJoin(doc.author_name),
|
||||
language: firstArrayValue(doc.language),
|
||||
publisher: firstArrayValue(doc.publisher),
|
||||
publishedDate: String(doc.first_publish_year ?? "") || null,
|
||||
isbn: firstArrayValue(doc.isbn)
|
||||
isbn: bestIsbn(doc.isbn, lookup.identifiers.isbn13)
|
||||
};
|
||||
}
|
||||
|
||||
private async lookupIsbn(isbn: string, expectedIsbn13: string | null): Promise<MetadataMatch | null> {
|
||||
const response = await fetch(`https://openlibrary.org/isbn/${encodeURIComponent(isbn)}.json`, {
|
||||
headers: { "User-Agent": "ReadaBook/0.1 local-library-manager" },
|
||||
signal: AbortSignal.timeout(4000)
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const edition = (await response.json()) as Record<string, unknown>;
|
||||
const author = await this.lookupAuthorName(edition.authors);
|
||||
return {
|
||||
title: stringValue(edition.title) ?? undefined,
|
||||
author,
|
||||
description: descriptionValue(edition.description),
|
||||
isbn: bestIsbn([...(asStringArray(edition.isbn_13)), ...(asStringArray(edition.isbn_10))], expectedIsbn13),
|
||||
language: languageValue(edition.languages),
|
||||
publisher: firstArrayValue(edition.publishers),
|
||||
publishedDate: stringValue(edition.publish_date)
|
||||
};
|
||||
}
|
||||
|
||||
private async lookupAuthorName(value: unknown): Promise<string | null> {
|
||||
const key = (Array.isArray(value) ? value[0] : undefined)?.key;
|
||||
if (typeof key !== "string") return null;
|
||||
const response = await fetch(`https://openlibrary.org${key}.json`, {
|
||||
headers: { "User-Agent": "ReadaBook/0.1 local-library-manager" },
|
||||
signal: AbortSignal.timeout(3000)
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const author = (await response.json()) as Record<string, unknown>;
|
||||
return stringValue(author.name);
|
||||
}
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function firstArrayValue(value: unknown): string | null {
|
||||
if (!Array.isArray(value) || !value.length) return null;
|
||||
return String(value[0]);
|
||||
}
|
||||
|
||||
function arrayJoin(value: unknown): string | null {
|
||||
return Array.isArray(value) && value.length ? value.map(String).join(", ") : null;
|
||||
}
|
||||
|
||||
function bestIsbn(value: unknown, expectedIsbn13: string | null): string | null {
|
||||
if (!Array.isArray(value)) return null;
|
||||
const values = value.map(String);
|
||||
return values.find((candidate) => toIsbn13(candidate) === expectedIsbn13) ?? values.find((candidate) => toIsbn13(candidate)) ?? null;
|
||||
}
|
||||
|
||||
function asStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map(String) : [];
|
||||
}
|
||||
|
||||
function descriptionValue(value: unknown): string | null {
|
||||
if (typeof value === "string") return value.trim() || null;
|
||||
if (typeof value === "object" && value && "value" in value) return stringValue(value.value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function languageValue(value: unknown): string | null {
|
||||
const key = (Array.isArray(value) ? value[0] : undefined)?.key;
|
||||
return typeof key === "string" ? key.split("/").pop() ?? null : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user