Phase 2 : service de lookup ISBN en cascade BnF puis OpenLibrary
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>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
using System.Xml.Linq;
|
||||
using MaBibli.Shared.Dtos;
|
||||
|
||||
namespace MaBibli.Api.Services.Isbn;
|
||||
|
||||
/// <summary>
|
||||
/// Lecture d'une réponse SRU de la BnF au schéma <c>dublincore</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Fonction pure : aucun accès réseau, ce qui permet de la tester sur des réponses réelles
|
||||
/// enregistrées sur disque.
|
||||
/// </remarks>
|
||||
public static class BnfSruParser
|
||||
{
|
||||
private static readonly XNamespace Srw = "http://www.loc.gov/zing/srw/";
|
||||
private static readonly XNamespace Dc = "http://purl.org/dc/elements/1.1/";
|
||||
|
||||
/// <summary>Nombre de notices annoncé par <c><srw:numberOfRecords></c>, ou 0.</summary>
|
||||
public static int LireNombreDeNotices(string xml)
|
||||
{
|
||||
var doc = XDocument.Parse(xml);
|
||||
var valeur = doc.Descendants(Srw + "numberOfRecords").FirstOrDefault()?.Value;
|
||||
return int.TryParse(valeur, out var n) ? n : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convertit la réponse SRU en candidats, champs déjà nettoyés de la ponctuation ISBD.
|
||||
/// </summary>
|
||||
/// <param name="xml">Corps de la réponse SRU.</param>
|
||||
/// <param name="isbnInterroge">Forme d'ISBN qui a produit cette réponse (13 ou 10).</param>
|
||||
/// <param name="urlCouverture">URL de couverture OpenLibrary à attacher (la BnF n'en fournit pas).</param>
|
||||
public static IReadOnlyList<CandidatLivre> Parser(string xml, string isbnInterroge, string? urlCouverture)
|
||||
{
|
||||
var doc = XDocument.Parse(xml);
|
||||
var candidats = new List<CandidatLivre>();
|
||||
|
||||
foreach (var record in doc.Descendants(Srw + "record"))
|
||||
{
|
||||
var data = record.Element(Srw + "recordData");
|
||||
if (data is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var titre = NettoyageIsbd.Titre(Premier(data, "title"));
|
||||
if (titre is null)
|
||||
{
|
||||
// Une notice sans titre exploitable n'a aucun intérêt pour un pré-remplissage.
|
||||
continue;
|
||||
}
|
||||
|
||||
candidats.Add(new CandidatLivre
|
||||
{
|
||||
Titre = titre,
|
||||
Auteur = NettoyageIsbd.Auteur(Premier(data, "creator")),
|
||||
Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")),
|
||||
Annee = Premier(data, "date"),
|
||||
Langue = Premier(data, "language"),
|
||||
CoverUrl = urlCouverture,
|
||||
Source = SourceMetadonnees.Bnf,
|
||||
IsbnInterroge = isbnInterroge,
|
||||
IdentifiantSource = record.Element(Srw + "recordIdentifier")?.Value?.Trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return candidats;
|
||||
}
|
||||
|
||||
private static string? Premier(XElement recordData, string nomLocal)
|
||||
{
|
||||
var valeur = recordData.Descendants(Dc + nomLocal).FirstOrDefault()?.Value?.Trim();
|
||||
return string.IsNullOrEmpty(valeur) ? null : valeur;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user