using MaBibli.Shared.Dtos; using MaBibli.Shared.Isbn; namespace MaBibli.Api.Services.Isbn; public interface IIsbnLookupService { /// /// Cherche toutes les notices correspondant à un ISBN. /// /// null si l'ISBN fourni n'est pas un ISBN valide. Task RechercherAsync(string isbnBrut, CancellationToken ct = default); } /// /// Cascade de lookup ISBN : BnF d'abord, OpenLibrary ensuite. /// /// /// L'ordre est dicté par CLAUDE.md : la collection est majoritairement francophone et le dépôt /// légal français donne à la BnF la meilleure couverture possible sur ce fonds, là où OpenLibrary /// est lacunaire. OpenLibrary n'est interrogée que si la BnF ne renvoie rien sous /// aucune des deux formes d'ISBN. /// public sealed class IsbnLookupService( IBnfClient bnf, IOpenLibraryClient openLibrary, ILogger logger) : IIsbnLookupService { public async Task RechercherAsync(string isbnBrut, CancellationToken ct = default) { var isbn = IsbnUtils.Normaliser(isbnBrut); // Un code en 977 est un PÉRIODIQUE : il porte un ISSN, donc un titre de revue, et // aucune recherche bib.isbn ne peut aboutir. Sans cette interception la cascade // s'exécutait en entier pour répondre « aucun résultat » — ce qui envoie l'utilisateur // chercher un défaut là où il n'y en a pas. On sort AVANT la validation ISBN parce // qu'un code de périodique est un EAN-13 parfaitement valide : seul son préfixe le trahit. if (CodePeriodique.EstCodePeriodique(isbn)) { return await DecrirePeriodiqueAsync(isbn!, ct); } if (!IsbnUtils.EstValide(isbn)) { return null; } // La couverture ne vient jamais de la BnF : son Dublin Core n'en fournit aucune. // On construit l'URL OpenLibrary quelle que soit la source des métadonnées. var couverture = IsbnUtils.UrlCouverture(isbn!); IsbnUtils.TryConvertirEnIsbn10(isbn, out var isbn10); var avertissements = new List(); // 1. BnF avec l'ISBN tel que scanné. var (candidats, avertissement) = await bnf.RechercherAsync(isbn!, couverture, ct); Ajouter(avertissements, avertissement); // 2. BnF avec l'ISBN-10 converti. Indispensable : la BnF indexe l'ISBN tel qu'imprimé, // et les ouvrages d'avant 2007 ne portent qu'un ISBN-10. if (candidats.Count == 0 && isbn10 is not null) { var (candidats10, avertissement10) = await bnf.RechercherAsync(isbn10, couverture, ct); Ajouter(avertissements, avertissement10); candidats = candidats10; } // 3. OpenLibrary en second rideau, seulement si la BnF n'a rien donné du tout. if (candidats.Count == 0) { var (candidatsOl, avertissementOl) = await openLibrary.RechercherAsync(isbn!, couverture, ct); Ajouter(avertissements, avertissementOl); candidats = candidatsOl; } logger.LogInformation( "Lookup ISBN {Isbn} : {Nombre} candidat(s), {Avertissements} avertissement(s)", isbn, candidats.Count, avertissements.Count); return new ResultatLookupIsbn { IsbnDemande = isbn!, Isbn10 = isbn10, Candidats = candidats, Avertissements = avertissements, }; } /// /// Nomme le périodique scanné, sans rien chercher du côté des livres. /// /// /// Le résultat ne porte jamais de candidat : le modèle de données ne gère pas les /// périodiques (ni numéro, ni date de parution), et cataloguer douze numéros d'une même /// revue créerait douze fiches identiques. L'objectif est uniquement de dire à l'utilisateur /// ce qu'il tient en main. Une BnF muette ou injoignable n'est pas un échec : on connaît /// l'ISSN de toute façon, puisqu'il se déduit du code-barres. /// private async Task DecrirePeriodiqueAsync(string ean, CancellationToken ct) { var issn = CodePeriodique.IssnDepuis(ean)!; var (periodique, avertissement) = await bnf.RechercherPeriodiqueAsync(issn, ct); var avertissements = new List(); Ajouter(avertissements, avertissement); logger.LogInformation( "Code {Ean} : périodique ISSN {Issn} ({Titre})", ean, issn, periodique?.Titre ?? "titre inconnu"); return new ResultatLookupIsbn { IsbnDemande = ean, Candidats = [], Avertissements = avertissements, Periodique = periodique ?? new PeriodiqueDetecte { Issn = issn }, }; } private static void Ajouter(List avertissements, string? message) { if (!string.IsNullOrEmpty(message)) { avertissements.Add(message); } } }