Recuperation des metadonnees d'un livre a partir de son ISBN, cote serveur uniquement (aucune interface, aucune persistance). Cascade : BnF en ISBN-13, puis BnF en ISBN-10 converti, puis OpenLibrary. La conversion 13 -> 10 est indispensable et non optionnelle : la BnF indexe l'ISBN tel qu'imprime, les ouvrages d'avant 2007 ne portent qu'un ISBN-10 et sont introuvables par l'EAN-13 que lit le scanner. Toutes les notices trouvees sont remontees (jusqu'a 5) : un meme ISBN peut correspondre a plusieurs reeditions, le choix revient a l'utilisateur. Le double appel /isbn puis /authors d'OpenLibrary est bien fait, avec repli sur l'oeuvre quand l'edition ne porte aucun auteur : c'est le bug de BookLogr (titre rempli, auteur vide) qu'il ne faut pas reproduire. La couverture vient toujours d'OpenLibrary, avec ?default=false pour obtenir un 404 plutot qu'une image placeholder. 87 tests xUnit, sur fixtures enregistrees : aucune dependance au reseau. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.6 KiB
C#
63 lines
2.6 KiB
C#
using MaBibli.Shared.Dtos;
|
|
|
|
namespace MaBibli.Api.Services.Isbn;
|
|
|
|
public interface IBnfClient
|
|
{
|
|
/// <summary>
|
|
/// Interroge le SRU de la BnF pour une forme d'ISBN donnée.
|
|
/// Renvoie une liste vide si la BnF ne connaît pas l'ISBN <b>ou</b> si elle est injoignable ;
|
|
/// dans ce second cas <paramref name="avertissement"/> explique pourquoi.
|
|
/// </summary>
|
|
Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
|
|
string isbn, string? urlCouverture, CancellationToken ct = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Client de l'API SRU du catalogue général de la BnF (gratuite, sans clé).
|
|
/// </summary>
|
|
public sealed class BnfClient(HttpClient http, ILogger<BnfClient> logger) : IBnfClient
|
|
{
|
|
/// <summary>Nom du client typé enregistré dans <c>IHttpClientFactory</c>.</summary>
|
|
public const string NomHttpClient = "bnf";
|
|
|
|
public const string UrlBase = "https://catalogue.bnf.fr/";
|
|
|
|
public async Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
|
|
string isbn, string? urlCouverture, CancellationToken ct = default)
|
|
{
|
|
// recordSchema=dublincore et non MARC : dc:title / dc:creator / dc:publisher se mappent
|
|
// directement, là où l'UNIMARC demanderait un décodage complet.
|
|
var url = "api/SRU"
|
|
+ "?version=1.2"
|
|
+ "&operation=searchRetrieve"
|
|
+ $"&query={Uri.EscapeDataString($"bib.isbn all \"{isbn}\"")}"
|
|
+ "&recordSchema=dublincore"
|
|
+ "&maximumRecords=5";
|
|
|
|
try
|
|
{
|
|
using var reponse = await http.GetAsync(url, ct);
|
|
if (!reponse.IsSuccessStatusCode)
|
|
{
|
|
logger.LogWarning("BnF a répondu {Code} pour l'ISBN {Isbn}", (int)reponse.StatusCode, isbn);
|
|
return ([], $"La BnF a répondu {(int)reponse.StatusCode} pour {isbn}.");
|
|
}
|
|
|
|
var xml = await reponse.Content.ReadAsStringAsync(ct);
|
|
return (BnfSruParser.Parser(xml, isbn, urlCouverture), null);
|
|
}
|
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
|
{
|
|
// Une source indisponible ne doit pas faire échouer la cascade : on bascule sur la suivante.
|
|
logger.LogWarning(ex, "BnF injoignable pour l'ISBN {Isbn}", isbn);
|
|
return ([], $"BnF injoignable ({ex.GetType().Name}) pour {isbn}.");
|
|
}
|
|
catch (System.Xml.XmlException ex)
|
|
{
|
|
logger.LogWarning(ex, "Réponse BnF illisible pour l'ISBN {Isbn}", isbn);
|
|
return ([], $"Réponse BnF illisible pour {isbn}.");
|
|
}
|
|
}
|
|
}
|