using System.Xml.Linq; using MaBibli.Shared.Dtos; namespace MaBibli.Api.Services.Isbn; /// /// Lecture d'une réponse SRU de la BnF au schéma dublincore. /// /// /// Fonction pure : aucun accès réseau, ce qui permet de la tester sur des réponses réelles /// enregistrées sur disque. /// 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/"; /// Nombre de notices annoncé par <srw:numberOfRecords>, ou 0. 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; } /// /// Convertit la réponse SRU en candidats, champs déjà nettoyés de la ponctuation ISBD. /// /// Corps de la réponse SRU. /// Forme d'ISBN qui a produit cette réponse (13 ou 10). /// URL de couverture OpenLibrary à attacher (la BnF n'en fournit pas). public static IReadOnlyList Parser(string xml, string isbnInterroge, string? urlCouverture) { var doc = XDocument.Parse(xml); var candidats = new List(); foreach (var record in doc.Descendants(Srw + "record")) { var data = record.Element(Srw + "recordData"); if (data is null) { continue; } // Plusieurs dc:creator sont possibles : un livre peut avoir plusieurs auteurs. var auteurs = Tous(data, "creator") .Select(NettoyageIsbd.Auteur) .Where(a => a is not null) .Select(a => a!) .Distinct() .ToList(); var titre = NettoyageIsbd.Titre(Premier(data, "title")); // Certaines notices recollent le nom de l'auteur en fin de titre sans le séparateur // ISBD — il faut donc les auteurs nettoyés avant de pouvoir finir le titre. titre = NettoyageIsbd.RetirerAuteurEnFin(titre, auteurs); if (titre is null) { // Une notice sans titre exploitable n'a aucun intérêt pour un pré-remplissage. continue; } var identifiant = record.Element(Srw + "recordIdentifier")?.Value?.Trim(); candidats.Add(new CandidatLivre { Titre = titre, Auteurs = auteurs, Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")), Annee = Premier(data, "date"), Langue = Premier(data, "language"), // ⚠️ Plusieurs dc:format cohabitent sur une notice (« 1 vol. (349 p.) », puis // « 30 cm ») : on prend le PREMIER qui donne une pagination, et non le premier // tout court, qui pourrait n'être qu'une dimension. NombrePages = Tous(data, "format") .Select(NettoyageIsbd.NombreDePages) .FirstOrDefault(pages => pages is not null), CoverUrl = urlCouverture, Source = SourceMetadonnees.Bnf, IsbnInterroge = isbnInterroge, IdentifiantSource = identifiant, UrlNotice = UrlNotice(identifiant), }); } return candidats; } private static string? UrlNotice(string? identifiant) => string.IsNullOrWhiteSpace(identifiant) ? null : $"https://catalogue.bnf.fr/{identifiant}"; /// /// Lit la première notice d'une réponse SRU interrogée par bib.issn. /// /// /// On ne renvoie qu'une notice, et sans laisser le choix : le but n'est pas de cataloguer /// la revue — le modèle de données ne le permet pas — mais de nommer ce que /// l'utilisateur vient de scanner. Un ISSN désigne un et un seul titre de périodique, donc /// les notices suivantes ne diraient rien de plus. /// /// Une notice de revue porte souvent deux dc:title : le titre clé qualifié /// (« Médor (Bruxelles) ») puis le titre développé avec sa mention de responsabilité. /// C'est le premier qu'on retient — le second est l'accroche commerciale, pas le nom. /// /// /// null si la réponse ne contient aucune notice exploitable. public static PeriodiqueDetecte? ParserPeriodique(string xml, string issn) { var doc = XDocument.Parse(xml); foreach (var record in doc.Descendants(Srw + "record")) { var data = record.Element(Srw + "recordData"); if (data is null) { continue; } var titre = NettoyageIsbd.TitrePeriodique(Premier(data, "title")); if (titre is null) { continue; } return new PeriodiqueDetecte { Issn = issn, Titre = titre, Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")), Annee = Premier(data, "date"), }; } return null; } private static string? Premier(XElement recordData, string nomLocal) => Tous(recordData, nomLocal).FirstOrDefault(); private static IEnumerable Tous(XElement recordData, string nomLocal) => recordData.Descendants(Dc + nomLocal) .Select(e => e.Value?.Trim()) .Where(v => !string.IsNullOrEmpty(v)) .Select(v => v!); }