using System.Net;
using System.Text.Json;
using MaBibli.Shared.Dtos;
namespace MaBibli.Api.Services.Isbn;
public interface IOpenLibraryClient
{
///
/// Cherche l'édition correspondant à un ISBN chez OpenLibrary, auteurs résolus.
/// Renvoie une liste vide si l'ISBN est inconnu ou la source injoignable.
///
Task<(IReadOnlyList Candidats, string? Avertissement)> RechercherAsync(
string isbn, string? urlCouverture, CancellationToken ct = default);
}
///
/// Client OpenLibrary — second rideau de la cascade, pour les livres étrangers et tout ce que
/// la BnF ne connaît pas.
///
///
/// Bug à ne pas reproduire (observé chez BookLogr) : /isbn/{isbn}.json ne donne pas
/// le nom de l'auteur mais une référence /authors/OL…A. Sans second appel, l'auteur reste
/// vide. Ce client fait ce second appel, et bascule sur l'œuvre rattachée quand l'édition ne
/// porte aucun auteur (cas mesuré : Introduction to Algorithms).
///
public sealed class OpenLibraryClient(HttpClient http, ILogger logger) : IOpenLibraryClient
{
public const string NomHttpClient = "openlibrary";
public const string UrlBase = "https://openlibrary.org/";
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
public async Task<(IReadOnlyList Candidats, string? Avertissement)> RechercherAsync(
string isbn, string? urlCouverture, CancellationToken ct = default)
{
try
{
// /isbn/{isbn}.json répond en 302 vers /books/OL…M.json quand le livre existe, 404 sinon.
// Le HttpClient suit la redirection par défaut.
var edition = await LireAsync($"isbn/{isbn}.json", ct);
if (edition is null)
{
return ([], null);
}
var noms = await ResoudreAuteursAsync(edition, ct);
var candidat = OpenLibraryMapper.VersCandidat(edition, noms, isbn, urlCouverture);
return (candidat is null ? [] : [candidat], null);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException)
{
logger.LogWarning(ex, "OpenLibrary injoignable ou illisible pour l'ISBN {Isbn}", isbn);
return ([], $"OpenLibrary injoignable ({ex.GetType().Name}) pour {isbn}.");
}
}
private async Task> ResoudreAuteursAsync(OpenLibraryEdition edition, CancellationToken ct)
{
var cles = OpenLibraryMapper.ClesAuteurs(edition);
if (cles.Count == 0)
{
// Repli : certaines éditions n'ont pas de champ `authors`, seule l'œuvre en porte.
var cleOeuvre = OpenLibraryMapper.CleOeuvre(edition);
if (cleOeuvre is not null)
{
var oeuvre = await LireAsync(cleOeuvre.TrimStart('/') + ".json", ct);
if (oeuvre is not null)
{
cles = OpenLibraryMapper.ClesAuteurs(oeuvre);
}
}
}
var noms = new List();
foreach (var cle in cles)
{
var auteur = await LireAsync(cle.TrimStart('/') + ".json", ct);
var nom = auteur is null ? null : OpenLibraryMapper.Nom(auteur);
if (nom is not null)
{
noms.Add(nom);
}
}
return noms;
}
private async Task LireAsync(string chemin, CancellationToken ct) where T : class
{
using var reponse = await http.GetAsync(chemin, ct);
if (reponse.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!reponse.IsSuccessStatusCode)
{
logger.LogWarning("OpenLibrary a répondu {Code} pour {Chemin}", (int)reponse.StatusCode, chemin);
return null;
}
await using var flux = await reponse.Content.ReadAsStreamAsync(ct);
return await JsonSerializer.DeserializeAsync(flux, Json, ct);
}
}