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!);
}