feat(api,web): providers ComicVine + MangaDex et pilotage de l'enrichissement

- Adaptateurs comic-vine et mangadex avec helper de fetch partagé,
  timeouts et fallback durcis sur les providers existants
- Scoring des correspondances amélioré, normalisation de la date de
  publication, chaîne de résolution des providers étendue
- Scanner : statuts par livre (scan/enrichissement) et page admin
  d'automatisation alignée

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-24 09:11:18 +02:00
parent 93bb40a8cd
commit d79f502dd2
25 changed files with 2812 additions and 231 deletions

View File

@ -1,14 +1,24 @@
import { describe, expect, it } from "vitest";
import type { MetadataSourcesConfigDto } from "@readabook/shared";
import { metadataSourcesPayload, moveSource, normalizeMetadataSources, scheduleSummary } from "./adminAutomation";
import type { AdminMetadataSourcesConfig } from "./adminAutomation";
import {
metadataSourcesPayload,
moveSource,
normalizeMetadataSources,
providerLabels,
providerUiMessage,
providerUiStateLabel,
scheduleSummary
} from "./adminAutomation";
const config: MetadataSourcesConfigDto = {
const config: AdminMetadataSourcesConfig = {
isbnPriorityEnabled: true,
sources: [
{ provider: "googlebooks", enabled: false, priority: 2, hasApiKey: true },
{ provider: "local", enabled: false, priority: 99, hasApiKey: false },
{ provider: "openlibrary", enabled: true, priority: 1, hasApiKey: false },
{ provider: "bnf", enabled: false, priority: 3, hasApiKey: false }
{ provider: "bnf", enabled: false, priority: 3, hasApiKey: false },
{ provider: "comicvine", enabled: true, priority: 4, hasApiKey: false, requiresCredentials: true },
{ provider: "mangadex", enabled: false, priority: 5, hasApiKey: false }
]
};
@ -27,17 +37,44 @@ describe("admin automation helpers", () => {
sources: [
{ provider: "openlibrary", enabled: true, priority: 1 },
{ provider: "googlebooks", enabled: false, priority: 2 },
{ provider: "bnf", enabled: false, priority: 3 }
{ provider: "bnf", enabled: false, priority: 3 },
{ provider: "comicvine", enabled: true, priority: 4 },
{ provider: "mangadex", enabled: false, priority: 5 }
]
});
});
it("moves only external providers", () => {
const moved = moveSource(normalizeMetadataSources(config).sources, "bnf", -1);
expect(moved.map((source) => source.provider)).toEqual(["local", "openlibrary", "bnf", "googlebooks"]);
expect(moved.map((source) => source.provider)).toEqual(["local", "openlibrary", "bnf", "googlebooks", "comicvine", "mangadex"]);
});
it("summarizes weekly schedules", () => {
expect(scheduleSummary({ frequency: "weekly", time: "04:30", dayOfWeek: 1 }, "Scan")).toBe("Scan chaque lundi a 04:30.");
});
it("adds Comic Vine and MangaDex when the backend omits them", () => {
const normalized = normalizeMetadataSources({
isbnPriorityEnabled: true,
sources: [{ provider: "local", enabled: true, priority: 0, hasApiKey: false }]
});
expect(normalized.sources.map((source) => source.provider)).toContain("comicvine");
expect(normalized.sources.map((source) => source.provider)).toContain("mangadex");
expect(providerLabels.comicvine).toBe("Comic Vine");
expect(providerLabels.mangadex).toBe("MangaDex");
});
it("labels provider configuration, rate limit and error states", () => {
expect(providerUiStateLabel({ provider: "comicvine", enabled: true, priority: 1, hasApiKey: false, requiresCredentials: true })).toBe(
"A configurer"
);
expect(providerUiMessage({ provider: "comicvine", enabled: true, priority: 1, hasApiKey: false, requiresCredentials: true })).toBe(
"Source activee, configuration incomplete."
);
expect(providerUiStateLabel({ provider: "mangadex", enabled: true, priority: 2, hasApiKey: false, rateLimited: true })).toBe("Limite");
expect(providerUiMessage({ provider: "mangadex", enabled: true, priority: 2, hasApiKey: false, status: "quota_exceeded" })).toBe(
"Quota ou limite temporaire atteint. ReadaBook reessaiera plus tard."
);
expect(providerUiStateLabel({ provider: "mangadex", enabled: true, priority: 2, hasApiKey: false, lastError: "500 stack" })).toBe("Erreur");
});
});