MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts, scan de code-barres, consultation hors-ligne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Isbn;
|
||||
|
||||
namespace MaBibli.Api.Services.Isbn;
|
||||
|
||||
public interface IIsbnLookupService
|
||||
{
|
||||
/// <summary>
|
||||
/// Cherche toutes les notices correspondant à un ISBN.
|
||||
/// </summary>
|
||||
/// <returns><c>null</c> si l'ISBN fourni n'est pas un ISBN valide.</returns>
|
||||
Task<ResultatLookupIsbn?> RechercherAsync(string isbnBrut, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cascade de lookup ISBN : <b>BnF d'abord, OpenLibrary ensuite</b>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// <b>aucune</b> des deux formes d'ISBN.
|
||||
/// </remarks>
|
||||
public sealed class IsbnLookupService(
|
||||
IBnfClient bnf,
|
||||
IOpenLibraryClient openLibrary,
|
||||
ILogger<IsbnLookupService> logger) : IIsbnLookupService
|
||||
{
|
||||
public async Task<ResultatLookupIsbn?> 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<string>();
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nomme le périodique scanné, sans rien chercher du côté des livres.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le résultat ne porte <b>jamais</b> 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.
|
||||
/// </remarks>
|
||||
private async Task<ResultatLookupIsbn> DecrirePeriodiqueAsync(string ean, CancellationToken ct)
|
||||
{
|
||||
var issn = CodePeriodique.IssnDepuis(ean)!;
|
||||
var (periodique, avertissement) = await bnf.RechercherPeriodiqueAsync(issn, ct);
|
||||
|
||||
var avertissements = new List<string>();
|
||||
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<string> avertissements, string? message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
avertissements.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user