Files
mabibli/MaBibli.Api/Services/Isbn/OpenLibraryMapper.cs
T
mathieuandClaude Opus 5 f8f30dbcf7 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>
2026-08-17 22:22:47 +02:00

108 lines
3.8 KiB
C#

using MaBibli.Shared.Dtos;
namespace MaBibli.Api.Services.Isbn;
/// <summary>
/// Transformation d'une édition OpenLibrary en candidat. Fonctions pures, testables sans réseau.
/// </summary>
public static class OpenLibraryMapper
{
/// <summary>
/// Clés d'auteurs portées par l'édition elle-même (<c>/authors/OL…A</c>), éventuellement vide.
/// </summary>
/// <remarks>
/// Ces clés ne sont <b>pas</b> des noms : chacune impose un appel à
/// <c>/authors/{id}.json</c>. Oublier ce second appel donne un titre rempli et un auteur vide
/// — symptôme exact observé chez BookLogr.
/// </remarks>
public static IReadOnlyList<string> ClesAuteurs(OpenLibraryEdition edition) =>
edition.Authors?
.Select(a => a.Key)
.Where(k => !string.IsNullOrWhiteSpace(k))
.Select(k => k!)
.ToList() ?? [];
/// <summary>Clés d'auteurs portées par l'œuvre, utilisées en repli.</summary>
public static IReadOnlyList<string> ClesAuteurs(OpenLibraryWork work) =>
work.Authors?
.Select(a => a.Author?.Key)
.Where(k => !string.IsNullOrWhiteSpace(k))
.Select(k => k!)
.ToList() ?? [];
/// <summary>Clé de l'œuvre rattachée (<c>/works/OL…W</c>), ou <c>null</c>.</summary>
public static string? CleOeuvre(OpenLibraryEdition edition) =>
edition.Works?.Select(w => w.Key).FirstOrDefault(k => !string.IsNullOrWhiteSpace(k));
/// <summary>Nom affichable d'un auteur OpenLibrary.</summary>
public static string? Nom(OpenLibraryAuthor auteur)
{
var nom = auteur.Name ?? auteur.PersonalName;
return string.IsNullOrWhiteSpace(nom) ? null : nom.Trim();
}
/// <summary>
/// Assemble le candidat final. <paramref name="nomsAuteurs"/> vient des appels
/// <c>/authors/{id}.json</c> déjà effectués par le client.
/// </summary>
public static CandidatLivre? VersCandidat(
OpenLibraryEdition edition,
IReadOnlyList<string> nomsAuteurs,
string isbnInterroge,
string? urlCouverture)
{
var titre = edition.Title?.Trim();
if (string.IsNullOrEmpty(titre))
{
return null;
}
if (!string.IsNullOrWhiteSpace(edition.Subtitle))
{
titre = $"{titre} : {edition.Subtitle.Trim()}";
}
var auteur = nomsAuteurs.Count > 0
? string.Join(", ", nomsAuteurs)
: NettoyerByStatement(edition.ByStatement);
return new CandidatLivre
{
Titre = titre,
Auteur = auteur,
Editeur = edition.Publishers?.FirstOrDefault()?.Trim().Trim('[', ']'),
Annee = string.IsNullOrWhiteSpace(edition.PublishDate) ? null : edition.PublishDate.Trim(),
Langue = edition.Languages?
.Select(l => l.Key)
.FirstOrDefault(k => !string.IsNullOrWhiteSpace(k))?
.Replace("/languages/", string.Empty),
CoverUrl = urlCouverture,
Source = SourceMetadonnees.OpenLibrary,
IsbnInterroge = isbnInterroge,
IdentifiantSource = edition.Key,
};
}
/// <summary>
/// Dernier repli quand aucun auteur n'est résolvable : la mention de responsabilité brute,
/// débarrassée de sa ponctuation finale (« Thomas H. Cormen ... [et al.]. »).
/// </summary>
private static string? NettoyerByStatement(string? brut)
{
var s = brut?.Trim();
if (string.IsNullOrEmpty(s))
{
return null;
}
var i = s.IndexOf(" ... ", StringComparison.Ordinal);
if (i >= 0)
{
s = s[..i];
}
s = s.TrimEnd('.', ' ', ',');
return s.Length == 0 ? null : s;
}
}